From ac06d92f7e6ec40fc9cbdf5c2fe867079cfc343c Mon Sep 17 00:00:00 2001 From: John Alhroos Date: Fri, 11 Jun 2010 13:43:22 +0000 Subject: [PATCH] Added keyboard navigation to PopupDateField #4506 svn changeset:13650/svn branch:6.4 --- .../themes/base/datefield/datefield.css | 3 + .../gwt/client/ui/FocusableFlexTable.java | 102 ++ .../gwt/client/ui/FocusableFlowPanel.java | 97 ++ .../gwt/client/ui/VCalendarPanel.java | 1199 ++++++++++++++--- .../gwt/client/ui/VPopupCalendar.java | 145 +- .../vaadin/terminal/gwt/client/ui/VTime.java | 221 ++- 6 files changed, 1535 insertions(+), 232 deletions(-) create mode 100644 src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java create mode 100644 src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java diff --git a/WebContent/VAADIN/themes/base/datefield/datefield.css b/WebContent/VAADIN/themes/base/datefield/datefield.css index f1c38513e1..6f69a91f5e 100644 --- a/WebContent/VAADIN/themes/base/datefield/datefield.css +++ b/WebContent/VAADIN/themes/base/datefield/datefield.css @@ -22,6 +22,9 @@ padding: 0; margin: 0; } +.v-datefield-calendarpanel:focus{ + outline:none; +} .v-datefield-calendarpanel-header td { text-align: center; } diff --git a/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java b/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java new file mode 100644 index 0000000000..a966aa8a16 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/FocusableFlexTable.java @@ -0,0 +1,102 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +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.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +/** + * Adds keyboard focus to {@link FlexPanel}. + */ +public class FocusableFlexTable extends FlexTable implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Default constructor. + */ + public FocusableFlexTable() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets the keyboard focus to the panel + * + * @param focus + * Should the panel have keyboard focus. If true the keyboard + * focus will be moved to the + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + public void focus() { + setFocus(true); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java b/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java new file mode 100644 index 0000000000..bb8857c374 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/FocusableFlowPanel.java @@ -0,0 +1,97 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +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.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +public class FocusableFlowPanel extends FlowPanel implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableFlowPanel() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets/Removes the keyboard focus to the panel. + * + * @param focus + * If set to true then the focus is moved to the panel, if set to + * false the focus is removed + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /** + * Focus the panel + */ + public void focus() { + setFocus(true); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/VCalendarPanel.java b/src/com/vaadin/terminal/gwt/client/ui/VCalendarPanel.java index 0c2cb056a6..bf753ce530 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VCalendarPanel.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VCalendarPanel.java @@ -10,18 +10,177 @@ import java.util.List; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +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.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.InlineHTML; import com.google.gwt.user.client.ui.MouseListener; import com.google.gwt.user.client.ui.MouseListenerCollection; import com.google.gwt.user.client.ui.SourcesMouseEvents; import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.BrowserInfo; import com.vaadin.terminal.gwt.client.DateTimeService; import com.vaadin.terminal.gwt.client.LocaleService; -public class VCalendarPanel extends FlexTable implements MouseListener { +public class VCalendarPanel extends FocusableFlexTable implements + MouseListener, KeyDownHandler, KeyPressHandler { + + /** + * Represents a Date button in the calendar + */ + private class VEventButton extends VNativeButton implements + SourcesMouseEvents { + + private MouseListenerCollection mouseListeners; + + /** + * Default constructor + */ + public VEventButton() { + super(); + sinkEvents(Event.FOCUSEVENTS | Event.KEYEVENTS | Event.ONCLICK + | Event.MOUSEEVENTS); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.FocusWidget#addMouseListener(com.google + * .gwt.user.client.ui.MouseListener) + */ + @Override + public void addMouseListener(MouseListener listener) { + if (mouseListeners == null) { + mouseListeners = new MouseListenerCollection(); + } + mouseListeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.FocusWidget#removeMouseListener(com + * .google.gwt.user.client.ui.MouseListener) + */ + @Override + public void removeMouseListener(MouseListener listener) { + if (mouseListeners != null) { + mouseListeners.remove(listener); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.VNativeButton#onBrowserEvent(com + * .google.gwt.user.client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + case Event.ONMOUSEUP: + case Event.ONMOUSEOUT: + if (mouseListeners != null) { + mouseListeners.fireMouseEvent(this, event); + } + break; + } + } + } + + /** + * Represents a click handler for when a user selects a date by using the + * mouse + */ + private class DateClickHandler implements ClickHandler { + + private final VCalendarPanel cal; + + public DateClickHandler(VCalendarPanel panel) { + cal = panel; + } + + public void selectDate(String date) { + try { + final Integer day = new Integer(date); + final Date newDate = cal.datefield.getShowingDate(); + newDate.setDate(day.intValue()); + if (!isEnabledDate(newDate)) { + return; + } + if (cal.datefield.getCurrentDate() == null) { + cal.datefield.setCurrentDate(new Date(newDate.getTime())); + + // Init variables with current time + datefield.getClient().updateVariable(cal.datefield.getId(), + "hour", newDate.getHours(), false); + datefield.getClient().updateVariable(cal.datefield.getId(), + "min", newDate.getMinutes(), false); + datefield.getClient().updateVariable(cal.datefield.getId(), + "sec", newDate.getSeconds(), false); + datefield.getClient().updateVariable(cal.datefield.getId(), + "msec", datefield.getMilliseconds(), false); + } + + cal.datefield.getCurrentDate().setTime(newDate.getTime()); + cal.datefield.getClient().updateVariable(cal.datefield.getId(), + "day", cal.datefield.getCurrentDate().getDate(), false); + cal.datefield.getClient().updateVariable(cal.datefield.getId(), + "month", cal.datefield.getCurrentDate().getMonth() + 1, + false); + cal.datefield.getClient().updateVariable(cal.datefield.getId(), + "year", + cal.datefield.getCurrentDate().getYear() + 1900, + cal.datefield.isImmediate()); + + if (datefield instanceof VTextualDate) { + ((VOverlay) getParent()).hide(); + } else { + updateCalendar(); + } + + } catch (final NumberFormatException e) { + // Not a number, ignore and stop here + return; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt + * .event.dom.client.ClickEvent) + */ + public void onClick(ClickEvent event) { + Object sender = event.getSource(); + Cell cell = cal.days.getCellForEvent(event); + if (sender != cal.days || cell == null || cell.getRowIndex() < 1 + || cell.getRowIndex() > 6 || !cal.datefield.isEnabled() + || cal.datefield.isReadonly() || cell.getCellIndex() < 1) { + return; + } + + final String text = cal.days.getText(cell.getRowIndex(), cell + .getCellIndex()); + if (text.equals(" ")) { + return; + } + + selectDate(text); + } + } private final VDateField datefield; @@ -49,18 +208,57 @@ public class VCalendarPanel extends FlexTable implements MouseListener { /* Needed to identify locale changes */ private String locale = LocaleService.getDefaultLocale(); + private int selectedRow; + private int selectedColumn; + + private boolean changingView = false; + + private final DateClickHandler dateClickHandler; + + private Timer mouseTimer; + public VCalendarPanel(VDateField parent) { + super(); + datefield = parent; setStyleName(VDateField.CLASSNAME + "-calendarpanel"); // buildCalendar(true); - days.addClickHandler(new DateClickHandler(this)); + + dateClickHandler = new DateClickHandler(this); + days.addClickHandler(dateClickHandler); + + /* + * 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); + } + } public VCalendarPanel(VDateField parent, Date min, Date max) { + super(); + datefield = parent; setStyleName(VDateField.CLASSNAME + "-calendarpanel"); - days.addClickHandler(new DateClickHandler(this)); + dateClickHandler = new DateClickHandler(this); + days.addClickHandler(dateClickHandler); + + /* + * 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); + } } private void buildCalendar(boolean forceRedraw) { @@ -118,9 +316,11 @@ public class VCalendarPanel extends FlexTable implements MouseListener { prevYear = new VEventButton(); prevYear.setHTML("«"); prevYear.setStyleName("v-button-prevyear"); + prevYear.getElement().setTabIndex(-1); nextYear = new VEventButton(); nextYear.setHTML("»"); nextYear.setStyleName("v-button-nextyear"); + nextYear.getElement().setTabIndex(-1); prevYear.addMouseListener(this); nextYear.addMouseListener(this); setWidget(0, 0, prevYear); @@ -130,9 +330,11 @@ public class VCalendarPanel extends FlexTable implements MouseListener { prevMonth = new VEventButton(); prevMonth.setHTML("‹"); prevMonth.setStyleName("v-button-prevmonth"); + prevMonth.getElement().setTabIndex(-1); nextMonth = new VEventButton(); nextMonth.setHTML("›"); nextMonth.setStyleName("v-button-nextmonth"); + nextMonth.getElement().setTabIndex(-1); prevMonth.addMouseListener(this); nextMonth.addMouseListener(this); setWidget(0, 3, nextMonth); @@ -217,10 +419,17 @@ public class VCalendarPanel extends FlexTable implements MouseListener { // date actually selected? Date selectedDate = datefield.getCurrentDate(); + // Showing is the date (year/month+year) that is currently shown in the // panel Date showing = datefield.getShowingDate(); + // Always show something + if (showing == null) { + showing = new Date(); + datefield.setShowingDate(showing); + } + // The day of month that is selected, -1 if no day of this month is // selected (i.e, showing another month/year than selected or nothing is // selected) @@ -230,7 +439,9 @@ public class VCalendarPanel extends FlexTable implements MouseListener { int dayOfMonthToday = -1; // Find out a day this month is selected - if (selectedDate != null + if (showing != null) { + dayOfMonthSelected = showing.getDate(); + } else if (selectedDate != null && selectedDate.getMonth() == showing.getMonth() && selectedDate.getYear() == showing.getYear()) { dayOfMonthSelected = selectedDate.getDate(); @@ -284,28 +495,31 @@ public class VCalendarPanel extends FlexTable implements MouseListener { } } - // Add CSS classes according to state - String cssClass = baseclass; + // Actually write the day of month + InlineHTML html = new InlineHTML(String.valueOf(dayOfMonth)); + html.setStylePrimaryName(baseclass); + html.setTitle(title); + // Add CSS classes according to state if (!isEnabledDate(curr)) { - cssClass += " " + baseclass + "-disabled"; + html.addStyleDependentName("disabled"); } if (dayOfMonthSelected == dayOfMonth) { - cssClass += " " + baseclass + "-selected"; + html.addStyleDependentName("selected"); + selectedRow = weekOfMonth; + selectedColumn = firstWeekdayColumn + dayOfWeek; } if (dayOfMonthToday == dayOfMonth) { - cssClass += " " + baseclass + "-today"; + html.addStyleDependentName("today"); } if (title.length() > 0) { - cssClass += " " + baseclass + "-entry"; + html.addStyleDependentName("entry"); } - // Actually write the day of month - days.setHTML(weekOfMonth, firstWeekdayColumn + dayOfWeek, - "" + dayOfMonth + ""); + days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek, + html); // ISO week numbers if requested if (!weekNumberProcessed[weekOfMonth]) { @@ -334,7 +548,7 @@ public class VCalendarPanel extends FlexTable implements MouseListener { private void buildTime(boolean forceRedraw) { if (time == null) { - time = new VTime(datefield); + time = new VTime(datefield, this); setWidget(2, 0, time); getFlexCellFormatter().setColSpan(2, 0, 5); getFlexCellFormatter().setStyleName(2, 0, @@ -344,19 +558,39 @@ public class VCalendarPanel extends FlexTable implements MouseListener { } /** - * - * @param forceRedraw - * Build all from scratch, in case of e.g. locale changes + * Updates the calendar and text field with the selected dates * */ public void updateCalendar() { - // Locale and resolution changes force a complete redraw - buildCalendar(locale != datefield.getCurrentLocale() - || resolution != datefield.getCurrentResolution()); + updateCalendarOnly(); if (datefield instanceof VTextualDate) { ((VTextualDate) datefield).buildDate(); } - locale = datefield.getCurrentLocale(); - resolution = datefield.getCurrentResolution(); + } + + /** + * Updates the popup only, does not affect the text field + */ + private void updateCalendarOnly() { + if (!changingView) { + changingView = true; + // Locale and resolution changes force a complete redraw + buildCalendar(locale != datefield.getCurrentLocale() + || resolution != datefield.getCurrentResolution()); + locale = datefield.getCurrentLocale(); + resolution = datefield.getCurrentResolution(); + + /* + * Wait a while before releasing the lock, so auto-repeated events + * isn't processed after the calendar is updated + */ + Timer changeTimer = new Timer() { + @Override + public void run() { + changingView = false; + } + }; + changeTimer.schedule(100); + } } private boolean isEnabledDate(Date date) { @@ -367,72 +601,123 @@ public class VCalendarPanel extends FlexTable implements MouseListener { return true; } + /** + * Selects the next month + */ + private void selectNextMonth() { + Date showingDate = datefield.getShowingDate(); + int currentMonth = showingDate.getMonth(); + showingDate.setMonth(currentMonth + 1); + int requestedMonth = (currentMonth + 1) % 12; + + /* + * If the selected date was e.g. 31.3 the new date would be 31.4 but + * this date is invalid so the new date will be 1.5. This is taken care + * of by decreasing the date until we have the correct month. + */ + while (showingDate.getMonth() != requestedMonth) { + showingDate.setDate(showingDate.getDate() - 1); + } + + updateCalendar(); + } + + /** + * Selects the previous month + */ + private void selectPreviousMonth() { + Date showingDate = datefield.getShowingDate(); + int currentMonth = showingDate.getMonth(); + showingDate.setMonth(currentMonth - 1); + + /* + * If the selected date was e.g. 31.12 the new date would be 31.11 but + * this date is invalid so the new date will be 1.12. This is taken care + * of by decreasing the date until we have the correct month. + */ + while (showingDate.getMonth() == currentMonth) { + showingDate.setDate(showingDate.getDate() - 1); + } + + updateCalendar(); + } + + /** + * Selects the previous year + */ + private void selectPreviousYear(int years) { + Date showingDate = datefield.getShowingDate(); + showingDate.setYear(showingDate.getYear() - years); + updateCalendar(); + } + + /** + * Selects the next year + */ + private void selectNextYear(int years) { + Date showingDate = datefield.getShowingDate(); + showingDate.setYear(showingDate.getYear() + years); + updateCalendar(); + } + + /** + * Handles a user click on the component + * + * @param sender + * The component that was clicked + * @param updateVariable + * Should the date field be updated + * + */ private void processClickEvent(Widget sender, boolean updateVariable) { if (!datefield.isEnabled() || datefield.isReadonly()) { return; } - Date showingDate = datefield.getShowingDate(); if (!updateVariable) { if (sender == prevYear) { - showingDate.setYear(showingDate.getYear() - 1); - updateCalendar(); + selectPreviousYear(1); } else if (sender == nextYear) { - showingDate.setYear(showingDate.getYear() + 1); - updateCalendar(); + selectNextYear(1); } else if (sender == prevMonth) { - int currentMonth = showingDate.getMonth(); - showingDate.setMonth(currentMonth - 1); - - /* - * If the selected date was e.g. 31.12 the new date would be - * 31.11 but this date is invalid so the new date will be 1.12. - * This is taken care of by decreasing the date until we have - * the correct month. - */ - while (showingDate.getMonth() == currentMonth) { - showingDate.setDate(showingDate.getDate() - 1); - } - - updateCalendar(); + selectPreviousMonth(); } else if (sender == nextMonth) { - int currentMonth = showingDate.getMonth(); - showingDate.setMonth(currentMonth + 1); - int requestedMonth = (currentMonth + 1) % 12; - - /* - * If the selected date was e.g. 31.3 the new date would be 31.4 - * but this date is invalid so the new date will be 1.5. This is - * taken care of by decreasing the date until we have the - * correct month. - */ - while (showingDate.getMonth() != requestedMonth) { - showingDate.setDate(showingDate.getDate() - 1); - } - - updateCalendar(); + selectNextMonth(); } } else { if (datefield.getCurrentResolution() == VDateField.RESOLUTION_YEAR || datefield.getCurrentResolution() == VDateField.RESOLUTION_MONTH) { - // Due to current UI, update variable if res=year/month - datefield.setCurrentDate(new Date(showingDate.getTime())); - if (datefield.getCurrentResolution() == VDateField.RESOLUTION_MONTH) { - datefield.getClient().updateVariable(datefield.getId(), - "month", datefield.getCurrentDate().getMonth() + 1, - false); - } - datefield.getClient().updateVariable(datefield.getId(), "year", - datefield.getCurrentDate().getYear() + 1900, - datefield.isImmediate()); - - /* Must update the value in the textfield also */ - updateCalendar(); + notifyServerOfChanges(); } } } - private Timer timer; + /** + * Send the date to the server + */ + private void notifyServerOfChanges() { + Date showingDate = datefield.getShowingDate(); + + // Due to current UI, update variable if res=year/month + datefield.setCurrentDate(new Date(showingDate.getTime())); + if (datefield.getCurrentResolution() == VDateField.RESOLUTION_MONTH) { + datefield.getClient().updateVariable(datefield.getId(), "month", + datefield.getCurrentDate().getMonth() + 1, false); + } + datefield.getClient().updateVariable(datefield.getId(), "year", + datefield.getCurrentDate().getYear() + 1900, + datefield.isImmediate()); + + /* Must update the value in the textfield also */ + updateCalendar(); + } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.MouseListener#onMouseDown(com.google.gwt + * .user.client.ui.Widget, int, int) + */ public void onMouseDown(final Widget sender, int x, int y) { // 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 @@ -440,192 +725,740 @@ public class VCalendarPanel extends FlexTable implements MouseListener { // timers are cancelled on mouseup or mouseout. if (sender instanceof VEventButton) { processClickEvent(sender, false); - timer = new Timer() { + mouseTimer = new Timer() { @Override public void run() { - timer = new Timer() { + mouseTimer = new Timer() { @Override public void run() { processClickEvent(sender, false); } }; - timer.scheduleRepeating(150); + mouseTimer.scheduleRepeating(150); } }; - timer.schedule(500); + mouseTimer.schedule(500); } } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.MouseListener#onMouseEnter(com.google.gwt + * .user.client.ui.Widget) + */ public void onMouseEnter(Widget sender) { } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.MouseListener#onMouseLeave(com.google.gwt + * .user.client.ui.Widget) + */ public void onMouseLeave(Widget sender) { - if (timer != null) { - timer.cancel(); + if (mouseTimer != null) { + mouseTimer.cancel(); } } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.MouseListener#onMouseMove(com.google.gwt + * .user.client.ui.Widget, int, int) + */ public void onMouseMove(Widget sender, int x, int y) { } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.MouseListener#onMouseUp(com.google.gwt. + * user.client.ui.Widget, int, int) + */ public void onMouseUp(Widget sender, int x, int y) { - if (timer != null) { - timer.cancel(); + if (mouseTimer != null) { + mouseTimer.cancel(); } processClickEvent(sender, true); } - private class VEventButton extends VNativeButton implements - SourcesMouseEvents { - private MouseListenerCollection mouseListeners; + public void setLimits(Date min, Date max) { + if (min != null) { + final Date d = new Date(min.getTime()); + d.setHours(0); + d.setMinutes(0); + d.setSeconds(1); + minDate = d; + } else { + minDate = null; + } + if (max != null) { + final Date d = new Date(max.getTime()); + d.setHours(24); + d.setMinutes(59); + d.setSeconds(59); + maxDate = d; + } else { + maxDate = null; + } + } - public VEventButton() { - super(); - sinkEvents(Event.FOCUSEVENTS | Event.KEYEVENTS | Event.ONCLICK - | Event.MOUSEEVENTS); + public void setCalendarEntrySource(CalendarEntrySource entrySource) { + this.entrySource = entrySource; + } + + public CalendarEntrySource getCalendarEntrySource() { + return entrySource; + } + + public interface CalendarEntrySource { + public List getEntries(Date date, int resolution); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeKeyCode(); + + if (handleNavigation(keycode, event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); } + } - @Override - public void addMouseListener(MouseListener listener) { - if (mouseListeners == null) { - mouseListeners = new MouseListenerCollection(); + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + public void onKeyPress(KeyPressEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + } + } + + /** + * Get the first valid weekday column index of this month + * + * @return A column index or zero if not found + */ + private int getFirstWeekdayColumn() { + Widget day = null; + for (int col = 1; col <= 7; col++) { + day = days.getWidget(1, col); + if (day != null) { + return col; } - mouseListeners.add(listener); } + return 0; + } - @Override - public void removeMouseListener(MouseListener listener) { - if (mouseListeners != null) { - mouseListeners.remove(listener); + /** + * Get the last valid weekday column index of this month + * + * @return A column index or zero if not found + */ + private int[] getLastWeekdayColumn() { + Widget day = null; + for (int row = days.getRowCount() - 1; row >= 1; row--) { + for (int col = 7; col >= 1; col--) { + day = days.getWidget(row, col); + if (day != null) { + return new int[] { row, col }; + } } } + return new int[] { 0, 0 }; + } - @Override - public void onBrowserEvent(Event event) { - super.onBrowserEvent(event); - switch (DOM.eventGetType(event)) { - case Event.ONMOUSEDOWN: - case Event.ONMOUSEUP: - case Event.ONMOUSEOUT: - if (mouseListeners != null) { - mouseListeners.fireMouseEvent(this, event); + /** + * 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()) { + selectNextYear(10); // Add 10 years + return true; + } + + else if (keycode == getForwardKey()) { + selectNextYear(1); // Add 1 year + return true; + } + + else if (keycode == getNextKey()) { + selectPreviousYear(10); // Subtract 10 years + return true; + } + + else if (keycode == getBackwardKey()) { + selectPreviousYear(1); // Subtract 1 year + return true; + + } else if (keycode == getSelectKey()) { + + // We need to notify the clickhandler of the selection + dateClickHandler.selectDate(String.valueOf(datefield + .getShowingDate().getDay())); + + // Send changes to server + notifyServerOfChanges(); + + if (datefield instanceof VTextualDate) { + ((VTextualDate) datefield).focus(); + } + return true; + + } else if (keycode == getResetKey()) { + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + showing.setTime(current.getTime()); + updateCalendar(); + return true; + + } else if (keycode == getCloseKey()) { + if (datefield instanceof VPopupCalendar) { + + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + + if (current != null && current != null) { + showing.setTime(current.getTime()); } - break; + + // Close popup.. + ((VPopupCalendar) datefield).closeCalendarPanel(); + + // ..and focus the textfield + ((VPopupCalendar) datefield).focus(); } + + return true; } + return false; } - private class DateClickHandler implements ClickHandler { + /** + * 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) { - private final VCalendarPanel cal; + // Ctrl selection not supported + if (ctrl) { + return false; - public DateClickHandler(VCalendarPanel panel) { - cal = panel; + } else if (keycode == getPreviousKey()) { + selectNextYear(1); // Add 1 year + return true; + + } else if (keycode == getForwardKey()) { + selectNextMonth(); // Add 1 month + return true; + + } else if (keycode == getNextKey()) { + selectPreviousYear(1); // Subtract 1 year + return true; + + } else if (keycode == getBackwardKey()) { + selectPreviousMonth(); // Subtract 1 month + return true; + + } else if (keycode == getSelectKey()) { + + // We need to notify the clickhandler of the selection + dateClickHandler.selectDate(String.valueOf(datefield + .getShowingDate().getDay())); + + // Send changes to server + notifyServerOfChanges(); + + if (datefield instanceof VTextualDate) { + ((VTextualDate) datefield).focus(); + } + + return true; + + } else if (keycode == getResetKey()) { + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + showing.setTime(current.getTime()); + updateCalendar(); + return true; + + } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) { + if (datefield instanceof VPopupCalendar) { + + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + + if (current != null && current != null) { + showing.setTime(current.getTime()); + } + + // Close popup.. + ((VPopupCalendar) datefield).closeCalendarPanel(); + + // ..and focus the textfield + ((VPopupCalendar) datefield).focus(); + } + + return true; } - public void onClick(ClickEvent event) { - Object sender = event.getSource(); - Cell cell = cal.days.getCellForEvent(event); - if (sender != cal.days || cell == null || cell.getRowIndex() < 1 - || cell.getRowIndex() > 6 || !cal.datefield.isEnabled() - || cal.datefield.isReadonly() || cell.getCellIndex() < 1) { - return; + 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; + } + + Date showingDate = datefield.getShowingDate(); + Widget currentSelection = days.getWidget(selectedRow, selectedColumn); + + /* + * Jumps to the next day. + */ + if (keycode == getForwardKey() && !shift) { + // Calculate new showing date + Date newCurrentDate = new Date(showingDate.getYear(), showingDate + .getMonth(), showingDate.getDate(), showingDate.getHours(), + showingDate.getMinutes(), showingDate.getSeconds()); + newCurrentDate.setDate(newCurrentDate.getDate() + 1); + + if (newCurrentDate.getMonth() == showingDate.getMonth()) { + // Month did not change, only move the selection + + showingDate.setDate(showingDate.getDate() + 1); + + if (currentSelection != null) { + currentSelection.removeStyleDependentName("selected"); + + // Calculate new selection + if (selectedColumn == 7) { + selectedColumn = 1; + selectedRow++; + } else { + selectedColumn++; + } + } + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); + + } else { + // If the month changed we need to re-render the calendar + showingDate.setDate(showingDate.getDate() + 1); + updateCalendarOnly(); + + selectedRow = 1; + selectedColumn = getFirstWeekdayColumn(); + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); } - final String text = cal.days.getText(cell.getRowIndex(), cell - .getCellIndex()); - if (text.equals(" ")) { - return; + return true; + + /* + * Jumps to the previous day + */ + } else if (keycode == getBackwardKey() && !shift) { + // Calculate new showing date + Date newCurrentDate = new Date(showingDate.getYear(), showingDate + .getMonth(), showingDate.getDate(), showingDate.getHours(), + showingDate.getMinutes(), showingDate.getSeconds()); + newCurrentDate.setDate(newCurrentDate.getDate() - 1); + + if (newCurrentDate.getMonth() == showingDate.getMonth()) { + // Month did not change, only move the selection + + showingDate.setDate(showingDate.getDate() - 1); + + if (currentSelection != null) { + currentSelection.removeStyleDependentName("selected"); + + // Calculate new selection + if (selectedColumn == 1) { + selectedColumn = 7; + selectedRow--; + } else { + selectedColumn--; + } + + } + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); + + } else { + // If the month changed we need to re-render the calendar + showingDate.setDate(showingDate.getDate() - 1); + updateCalendarOnly(); + + int[] pos = getLastWeekdayColumn(); + selectedRow = pos[0]; + selectedColumn = pos[1]; + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); } - try { - final Integer day = new Integer(text); - final Date newDate = cal.datefield.getShowingDate(); - newDate.setDate(day.intValue()); - if (!isEnabledDate(newDate)) { - return; + return true; + + /* + * Jumps one week back in the calendar + */ + } else if (keycode == getPreviousKey() && !shift) { + // Calculate new showing date + Date newCurrentDate = new Date(showingDate.getYear(), showingDate + .getMonth(), showingDate.getDate(), showingDate.getHours(), + showingDate.getMinutes(), showingDate.getSeconds()); + newCurrentDate.setDate(newCurrentDate.getDate() - 7); + + if (newCurrentDate.getMonth() == showingDate.getMonth() + && selectedRow > 1) { + // Month did not change, only move the selection + + showingDate.setDate(showingDate.getDate() - 7); + + if (currentSelection != null) { + currentSelection.removeStyleDependentName("selected"); } - if (cal.datefield.getCurrentDate() == null) { - cal.datefield.setCurrentDate(new Date(newDate.getTime())); - // Init variables with current time - datefield.getClient().updateVariable(cal.datefield.getId(), - "hour", newDate.getHours(), false); - datefield.getClient().updateVariable(cal.datefield.getId(), - "min", newDate.getMinutes(), false); - datefield.getClient().updateVariable(cal.datefield.getId(), - "sec", newDate.getSeconds(), false); - datefield.getClient().updateVariable(cal.datefield.getId(), - "msec", datefield.getMilliseconds(), false); + selectedRow--; + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); + + } else { + // If the month changed we need to re-render the calendar + showingDate.setDate(showingDate.getDate() - 7); + updateCalendarOnly(); + + int[] pos = getLastWeekdayColumn(); + selectedRow = pos[0]; + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + if (currentSelection == null) { + selectedRow--; + currentSelection = days.getWidget(selectedRow, + selectedColumn); } - cal.datefield.getCurrentDate().setTime(newDate.getTime()); - cal.datefield.getClient().updateVariable(cal.datefield.getId(), - "day", cal.datefield.getCurrentDate().getDate(), false); - cal.datefield.getClient().updateVariable(cal.datefield.getId(), - "month", cal.datefield.getCurrentDate().getMonth() + 1, - false); - cal.datefield.getClient().updateVariable(cal.datefield.getId(), - "year", - cal.datefield.getCurrentDate().getYear() + 1900, - cal.datefield.isImmediate()); + currentSelection.addStyleDependentName("selected"); + } - if (datefield instanceof VTextualDate - && resolution < VDateField.RESOLUTION_HOUR) { - ((VOverlay) getParent()).hide(); - } else { - updateCalendar(); + return true; + + /* + * Jumps one week forward in the calendar + */ + } else if (keycode == getNextKey() && !ctrl && !shift) { + // Calculate new showing date + Date newCurrentDate = new Date(showingDate.getYear(), showingDate + .getMonth(), showingDate.getDate(), showingDate.getHours(), + showingDate.getMinutes(), showingDate.getSeconds()); + newCurrentDate.setDate(newCurrentDate.getDate() + 7); + + if (newCurrentDate.getMonth() == showingDate.getMonth()) { + // Month did not change, only move the selection + + showingDate.setDate(showingDate.getDate() + 7); + + if (currentSelection != null) { + currentSelection.removeStyleDependentName("selected"); } - } catch (final NumberFormatException e) { - // Not a number, ignore and stop here - return; + selectedRow++; + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + currentSelection.addStyleDependentName("selected"); + + } else { + // If the month changed we need to re-render the calendar + showingDate.setDate(showingDate.getDate() + 7); + updateCalendarOnly(); + + selectedRow = 1; + + // Set new selection + currentSelection = days.getWidget(selectedRow, selectedColumn); + if (currentSelection == null) { + selectedRow++; + currentSelection = days.getWidget(selectedRow, + selectedColumn); + } + + currentSelection.addStyleDependentName("selected"); } - } + return true; + + /* + * Selects the date that is chosen + */ + } else if (keycode == getSelectKey() && !shift) { + InlineHTML selection = (InlineHTML) days.getWidget(selectedRow, + selectedColumn); + dateClickHandler.selectDate(selection.getText()); + + if (datefield instanceof VTextualDate) { + ((VTextualDate) datefield).focus(); + } + + return true; + + /* + * Closes the date popup + */ + } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) { + if (datefield instanceof VPopupCalendar) { + + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + + if (current != null && current != null) { + showing.setTime(current.getTime()); + } + + // Close popup.. + ((VPopupCalendar) datefield).closeCalendarPanel(); + + // ..and focus the textfield + ((VPopupCalendar) datefield).focus(); + + } + + return true; + + /* + * Selects the next month + */ + } else if (shift && keycode == getForwardKey()) { + selectNextMonth(); + return true; + + /* + * Selects the previous month + */ + } else if (shift && keycode == getBackwardKey()) { + selectPreviousMonth(); + return true; + + /* + * Selects the next year + */ + } else if (shift && keycode == getPreviousKey()) { + selectNextYear(1); + return true; + + /* + * Selects the previous year + */ + } else if (shift && keycode == getNextKey()) { + selectPreviousYear(1); + return true; + + /* + * Resets the selection + */ + } else if (keycode == getResetKey() && !shift) { + // Restore showing date the selected date + Date showing = datefield.getShowingDate(); + Date current = datefield.getCurrentDate(); + showing.setTime(current.getTime()); + updateCalendar(); + return true; + } + + return false; } - public void setLimits(Date min, Date max) { - if (min != null) { - final Date d = new Date(min.getTime()); - d.setHours(0); - d.setMinutes(0); - d.setSeconds(1); - minDate = d; - } else { - minDate = null; + /** + * 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 + */ + @SuppressWarnings("deprecation") + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (!datefield.isEnabled() || datefield.isReadonly() || changingView) { + return false; } - if (max != null) { - final Date d = new Date(max.getTime()); - d.setHours(24); - d.setMinutes(59); - d.setSeconds(59); - maxDate = d; - } else { - maxDate = null; + + 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 { + // Do not close the window on tab key but move the + // focus down to the time + if (keycode == KeyCodes.KEY_TAB) { + time.setFocus(true); + return true; + } + + return handleNavigationDayMode(keycode, ctrl, shift); + } + } - public void setCalendarEntrySource(CalendarEntrySource entrySource) { - this.entrySource = entrySource; + /** + * 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; } - public CalendarEntrySource getCalendarEntrySource() { - return entrySource; + /** + * Returns the select key which selects the date. 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; } - public interface CalendarEntrySource { - public List getEntries(Date date, int resolution); + /** + * 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; } /** - * Sets focus to Calendar panel. + * 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. * - * @param focus + * @return */ - public void setFocus(boolean focus) { - nextYear.setFocus(focus); + protected int getPreviousKey() { + return KeyCodes.KEY_UP; } } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java index b8a004e699..7c87927fd3 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java @@ -6,9 +6,11 @@ package com.vaadin.terminal.gwt.client.ui; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; @@ -18,12 +20,22 @@ import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.UIDL; +/** + * Represents a date selection component with a text field and a popup date + * selector. + * + * Note: To change the keyboard assignments used in the popup dialog you + * should extend com.vaadin.terminal.gwt.client.ui.VCalendarPanel + * and then pass set it by calling the + * setCalendarPanel(VCalendarPanel panel) method. + * + */ public class VPopupCalendar extends VTextualDate implements Paintable, Field, ClickHandler, CloseHandler { private final Button calendarToggle; - private final VCalendarPanel calendar; + private VCalendarPanel calendar; private final VOverlay popup; private boolean open = false; @@ -36,19 +48,31 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, calendarToggle.setStyleName(CLASSNAME + "-button"); calendarToggle.setText(""); calendarToggle.addClickHandler(this); + calendarToggle.getElement().setTabIndex(-1); add(calendarToggle); calendar = new VCalendarPanel(this); + popup = new VOverlay(true, true, true); popup.setStyleName(VDateField.CLASSNAME + "-popup"); popup.setWidget(calendar); - popup.addCloseHandler(this); + popup.addCloseHandler(this); DOM.setElementProperty(calendar.getElement(), "id", "PID_VAADIN_POPUPCAL"); + sinkEvents(Event.ONKEYDOWN); + } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.VTextualDate#updateFromUIDL(com.vaadin + * .terminal.gwt.client.UIDL, + * com.vaadin.terminal.gwt.client.ApplicationConnection) + */ @Override public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { boolean lastReadOnlyState = readonly; @@ -64,13 +88,6 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, } calendarToggle.setEnabled(enabled); - // not a FocusWidget -> needs own tabindex handling - if (uidl.hasAttribute("tabindex")) { - // Set the same tab index as for the textfield. Tabbing then works - // as expected. - calendarToggle.setTabIndex(uidl.getIntAttribute("tabindex")); - } - if (readonly) { calendarToggle.addStyleName(CLASSNAME + "-button-readonly"); } else { @@ -84,16 +101,42 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, calendarToggle.setEnabled(true); } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String) + */ @Override public void setStyleName(String style) { // make sure the style is there before size calculation super.setStyleName(style + " " + CLASSNAME + "-popupcalendar"); } - public void onClick(ClickEvent event) { - if (event.getSource() == calendarToggle && !open && !readonly) { + /** + * Set the popup panel to be displayed when clicking the calendar button. + * This is usually used when we want to extend the VCalendarPanel and use + * new keyboard bindings. + * + * @param panel + * The custom calendar panel + */ + public void setCalendarPanel(VCalendarPanel panel) { + if (panel != null) { + calendar = panel; + calendar.updateCalendar(); + } + } + + /** + * Opens the calendar panel popup + */ + public void openCalendarPanel() { + + if (!open && !readonly) { open = true; calendar.updateCalendar(); + // clear previous values popup.setWidth(""); popup.setHeight(""); @@ -140,12 +183,43 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, popup.setPopupPosition(l, t + calendarToggle.getOffsetHeight() + 2); - setFocus(true); + /* We have to wait a while before focusing + * since the popup needs to be opened before + * we can focus + */ + Timer focusTimer = new Timer() { + @Override + public void run() { + setFocus(true); + } + }; + + focusTimer.schedule(100); } }); } } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event + * .dom.client.ClickEvent) + */ + public void onClick(ClickEvent event) { + if (event.getSource() == calendarToggle) { + openCalendarPanel(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt + * .event.logical.shared.CloseEvent) + */ public void onClose(CloseEvent event) { if (event.getSource() == popup) { buildDate(); @@ -169,6 +243,11 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, calendar.setFocus(focus); } + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.VTextualDate#getFieldExtraWidth() + */ @Override protected int getFieldExtraWidth() { if (fieldExtraWidth < 0) { @@ -178,6 +257,11 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, return fieldExtraWidth; } + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.VTextualDate#buildDate() + */ @Override protected void buildDate() { // Save previous value @@ -189,4 +273,41 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, setText(previousValue); } } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.VDateField#onBrowserEvent(com.google + * .gwt.user.client.Event) + */ + @Override + public void onBrowserEvent(com.google.gwt.user.client.Event event) { + super.onBrowserEvent(event); + if (DOM.eventGetType(event) == Event.ONKEYDOWN + && event.getKeyCode() == getOpenCalenderPanelKey()) { + openCalendarPanel(); + event.preventDefault(); + } + } + + /** + * Get the key code that opens the calendar panel. By default it is the down + * key but you can override this to be whatever you like + * + * @return + */ + protected int getOpenCalenderPanelKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Closes the open popup panel + */ + public void closeCalendarPanel() { + if (open) { + popup.hide(true); + } + } + } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTime.java b/src/com/vaadin/terminal/gwt/client/ui/VTime.java index 6f60037655..99cf94b4c4 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTime.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTime.java @@ -8,13 +8,25 @@ import java.util.Date; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; -import com.google.gwt.user.client.ui.FlowPanel; +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.user.client.Event; +import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.ListBox; +import com.vaadin.terminal.gwt.client.BrowserInfo; -public class VTime extends FlowPanel implements ChangeHandler { +public class VTime extends FocusableFlowPanel implements ChangeHandler, + KeyPressHandler, KeyDownHandler, FocusHandler { private final VDateField datefield; + private final VCalendarPanel calendar; + private ListBox hours; private ListBox mins; @@ -23,18 +35,85 @@ public class VTime extends FlowPanel implements ChangeHandler { private ListBox msec; - private ListBox ampm; + private AMPMListBox ampm; private int resolution = VDateField.RESOLUTION_HOUR; private boolean readonly; - public VTime(VDateField parent) { + /** + * The AM/PM Listbox yields the keyboard focus to the calendar + */ + private class AMPMListBox extends ListBox { + public AMPMListBox() { + super(); + sinkEvents(Event.ONKEYDOWN); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + if (event.getKeyCode() == KeyCodes.KEY_TAB) { + event.preventDefault(); + + /* + * Wait until the current event has been processed. If the timer + * is left out the focus will move to the VTime-class not the + * panel itself. Weird. + */ + Timer t = new Timer() { + @Override + public void run() { + calendar.setFocus(true); + } + }; + t.schedule(1); + } + } + } + + /** + * Constructor + * + * @param parent + * The DateField related to this instance + * @param panel + * The panel where this this instance is embedded + */ + public VTime(VDateField parent, VCalendarPanel panel) { super(); datefield = parent; + calendar = panel; setStyleName(VDateField.CLASSNAME + "-time"); + + /* + * 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); } + /** + * Constructs the ListBoxes and updates their value + * + * @param redraw + * Should new instances of the listboxes be created + */ private void buildTime(boolean redraw) { final boolean thc = datefield.getDateTimeService().isTwelveHourClock(); if (redraw) { @@ -47,10 +126,11 @@ public class VTime extends FlowPanel implements ChangeHandler { } hours.addChangeHandler(this); if (thc) { - ampm = new ListBox(); + ampm = new AMPMListBox(); ampm.setStyleName(VNativeSelect.CLASSNAME); final String[] ampmText = datefield.getDateTimeService() .getAmPmStrings(); + calendar.setFocus(true); ampm.addItem(ampmText[0]); ampm.addItem(ampmText[1]); ampm.addChangeHandler(this); @@ -151,7 +231,7 @@ public class VTime extends FlowPanel implements ChangeHandler { } // Update times - Date cdate = datefield.getCurrentDate(); + Date cdate = datefield.getShowingDate(); boolean selected = true; if (cdate == null) { cdate = new Date(); @@ -236,6 +316,12 @@ public class VTime extends FlowPanel implements ChangeHandler { } + /** + * Update the time ListBoxes + * + * @param redraw + * Should new instances of the listboxes be created + */ public void updateTime(boolean redraw) { buildTime(redraw || resolution != datefield.getCurrentResolution() || readonly != datefield.isReadonly()); @@ -246,6 +332,13 @@ public class VTime extends FlowPanel implements ChangeHandler { readonly = datefield.isReadonly(); } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.ChangeHandler#onChange(com.google.gwt + * .event.dom.client.ChangeEvent) + */ public void onChange(ChangeEvent event) { if (datefield.getCurrentDate() == null) { // was null on server, need to set @@ -257,61 +350,115 @@ public class VTime extends FlowPanel implements ChangeHandler { datefield.setCurrentDate(new Date(now.getTime())); // Init variables with current time - datefield.getClient().updateVariable(datefield.getId(), "year", - now.getYear() + 1900, false); - datefield.getClient().updateVariable(datefield.getId(), "month", - now.getMonth() + 1, false); - datefield.getClient().updateVariable(datefield.getId(), "day", - now.getDate(), false); - datefield.getClient().updateVariable(datefield.getId(), "hour", - now.getHours(), false); - datefield.getClient().updateVariable(datefield.getId(), "min", - now.getMinutes(), false); - datefield.getClient().updateVariable(datefield.getId(), "sec", - now.getSeconds(), false); - datefield.getClient().updateVariable(datefield.getId(), "msec", - datefield.getMilliseconds(), false); + notifyServerOfChanges(); } if (event.getSource() == hours) { int h = hours.getSelectedIndex(); if (datefield.getDateTimeService().isTwelveHourClock()) { h = h + ampm.getSelectedIndex() * 12; } - datefield.getCurrentDate().setHours(h); datefield.getShowingDate().setHours(h); - datefield.getClient().updateVariable(datefield.getId(), "hour", h, - datefield.isImmediate()); updateTime(false); } else if (event.getSource() == mins) { final int m = mins.getSelectedIndex(); - datefield.getCurrentDate().setMinutes(m); datefield.getShowingDate().setMinutes(m); - datefield.getClient().updateVariable(datefield.getId(), "min", m, - datefield.isImmediate()); updateTime(false); } else if (event.getSource() == sec) { final int s = sec.getSelectedIndex(); - datefield.getCurrentDate().setSeconds(s); datefield.getShowingDate().setSeconds(s); - datefield.getClient().updateVariable(datefield.getId(), "sec", s, - datefield.isImmediate()); updateTime(false); } else if (event.getSource() == msec) { final int ms = msec.getSelectedIndex(); - datefield.setMilliseconds(ms); datefield.setShowingMilliseconds(ms); - datefield.getClient().updateVariable(datefield.getId(), "msec", ms, - datefield.isImmediate()); updateTime(false); } else if (event.getSource() == ampm) { - final int h = hours.getSelectedIndex() + ampm.getSelectedIndex() - * 12; - datefield.getCurrentDate().setHours(h); + final int h = hours.getSelectedIndex() + + (ampm.getSelectedIndex() * 12); datefield.getShowingDate().setHours(h); - datefield.getClient().updateVariable(datefield.getId(), "hour", h, - datefield.isImmediate()); updateTime(false); } } + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + public void onKeyPress(KeyPressEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + + if (calendar != null) { + if (keycode == calendar.getSelectKey() + || keycode == calendar.getCloseKey()) { + if (keycode == calendar.getSelectKey()) { + notifyServerOfChanges(); + } + + calendar.handleNavigation(keycode, event.getNativeEvent() + .getCtrlKey() + || event.getNativeEvent().getMetaKey(), event + .getNativeEvent().getShiftKey()); + return; + } + } + + event.stopPropagation(); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode != calendar.getCloseKey() + && keycode != calendar.getSelectKey()) { + event.stopPropagation(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + public void onFocus(FocusEvent event) { + event.preventDefault(); + + // Delegate focus to the hour select + hours.setFocus(true); + } + + /** + * Update the variables server side + */ + public void notifyServerOfChanges() { + /* + * Just update the variables, don't send any thing. The calendar panel + * will make the request when the panel is closed. + */ + Date now = datefield.getCurrentDate(); + datefield.getClient().updateVariable(datefield.getId(), "year", + now.getYear() + 1900, false); + datefield.getClient().updateVariable(datefield.getId(), "month", + now.getMonth() + 1, false); + datefield.getClient().updateVariable(datefield.getId(), "day", + now.getDate(), false); + datefield.getClient().updateVariable(datefield.getId(), "hour", + now.getHours(), false); + datefield.getClient().updateVariable(datefield.getId(), "min", + now.getMinutes(), false); + datefield.getClient().updateVariable(datefield.getId(), "sec", + now.getSeconds(), false); + datefield.getClient().updateVariable(datefield.getId(), "msec", + datefield.getShowingMilliseconds(), false); + } + } -- 2.39.5