View Javadoc
1   /*
2    * Copyright 2000-2013 Vaadin Ltd.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * the License at
7    * 
8    * http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  
17  package com.vaadin.client.ui;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Date;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Set;
26  
27  import com.google.gwt.aria.client.Roles;
28  import com.google.gwt.core.client.Scheduler;
29  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
30  import com.google.gwt.dom.client.Style;
31  import com.google.gwt.dom.client.Style.Display;
32  import com.google.gwt.dom.client.Style.Unit;
33  import com.google.gwt.event.dom.client.BlurEvent;
34  import com.google.gwt.event.dom.client.BlurHandler;
35  import com.google.gwt.event.dom.client.ClickEvent;
36  import com.google.gwt.event.dom.client.ClickHandler;
37  import com.google.gwt.event.dom.client.FocusEvent;
38  import com.google.gwt.event.dom.client.FocusHandler;
39  import com.google.gwt.event.dom.client.KeyCodes;
40  import com.google.gwt.event.dom.client.KeyDownEvent;
41  import com.google.gwt.event.dom.client.KeyDownHandler;
42  import com.google.gwt.event.dom.client.KeyUpEvent;
43  import com.google.gwt.event.dom.client.KeyUpHandler;
44  import com.google.gwt.event.dom.client.LoadEvent;
45  import com.google.gwt.event.dom.client.LoadHandler;
46  import com.google.gwt.event.logical.shared.CloseEvent;
47  import com.google.gwt.event.logical.shared.CloseHandler;
48  import com.google.gwt.user.client.Command;
49  import com.google.gwt.user.client.DOM;
50  import com.google.gwt.user.client.Element;
51  import com.google.gwt.user.client.Event;
52  import com.google.gwt.user.client.Timer;
53  import com.google.gwt.user.client.Window;
54  import com.google.gwt.user.client.ui.Composite;
55  import com.google.gwt.user.client.ui.FlowPanel;
56  import com.google.gwt.user.client.ui.HTML;
57  import com.google.gwt.user.client.ui.Image;
58  import com.google.gwt.user.client.ui.PopupPanel;
59  import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
60  import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
61  import com.google.gwt.user.client.ui.TextBox;
62  import com.vaadin.client.ApplicationConnection;
63  import com.vaadin.client.BrowserInfo;
64  import com.vaadin.client.ComponentConnector;
65  import com.vaadin.client.ComputedStyle;
66  import com.vaadin.client.ConnectorMap;
67  import com.vaadin.client.Focusable;
68  import com.vaadin.client.UIDL;
69  import com.vaadin.client.Util;
70  import com.vaadin.client.VConsole;
71  import com.vaadin.client.ui.aria.AriaHelper;
72  import com.vaadin.client.ui.aria.HandlesAriaCaption;
73  import com.vaadin.client.ui.aria.HandlesAriaInvalid;
74  import com.vaadin.client.ui.aria.HandlesAriaRequired;
75  import com.vaadin.client.ui.menubar.MenuBar;
76  import com.vaadin.client.ui.menubar.MenuItem;
77  import com.vaadin.shared.AbstractComponentState;
78  import com.vaadin.shared.EventId;
79  import com.vaadin.shared.ui.ComponentStateUtil;
80  import com.vaadin.shared.ui.combobox.FilteringMode;
81  
82  /**
83   * Client side implementation of the Select component.
84   * 
85   * TODO needs major refactoring (to be extensible etc)
86   */
87  @SuppressWarnings("deprecation")
88  public class VFilterSelectPatched extends Composite implements Field, KeyDownHandler,
89          KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable,
90          SubPartAware, HandlesAriaCaption, HandlesAriaInvalid,
91          HandlesAriaRequired {
92  
93      /**
94       * Represents a suggestion in the suggestion popup box
95       */
96      public class FilterSelectSuggestion implements Suggestion, Command {
97  
98          private final String key;
99          private final String caption;
100         private String iconUri;
101 
102         /**
103          * Constructor
104          * 
105          * @param uidl
106          *            The UIDL recieved from the server
107          */
108         public FilterSelectSuggestion(UIDL uidl) {
109             key = uidl.getStringAttribute("key");
110             caption = uidl.getStringAttribute("caption");
111             if (uidl.hasAttribute("icon")) {
112                 iconUri = client.translateVaadinUri(uidl
113                         .getStringAttribute("icon"));
114             }
115         }
116 
117         /**
118          * Gets the visible row in the popup as a HTML string. The string
119          * contains an image tag with the rows icon (if an icon has been
120          * specified) and the caption of the item
121          */
122 
123         @Override
124         public String getDisplayString() {
125             final StringBuffer sb = new StringBuffer();
126             if (iconUri != null) {
127                 sb.append("<img src=\"");
128                 sb.append(Util.escapeAttribute(iconUri));
129                 sb.append("\" alt=\"\" class=\"v-icon\" />");
130             }
131             String content;
132             if ("".equals(caption)) {
133                 // Ensure that empty options use the same height as other
134                 // options and are not collapsed (#7506)
135                 content = "&nbsp;";
136             } else {
137                 content = Util.escapeHTML(caption);
138             }
139             sb.append("<span>" + content + "</span>");
140             return sb.toString();
141         }
142 
143         /**
144          * Get a string that represents this item. This is used in the text box.
145          */
146 
147         @Override
148         public String getReplacementString() {
149             return caption;
150         }
151 
152         /**
153          * Get the option key which represents the item on the server side.
154          * 
155          * @return The key of the item
156          */
157         public String getOptionKey() {
158             return key;
159         }
160 
161         /**
162          * Get the URI of the icon. Used when constructing the displayed option.
163          * 
164          * @return
165          */
166         public String getIconUri() {
167             return iconUri;
168         }
169 
170         /**
171          * Executes a selection of this item.
172          */
173 
174         @Override
175         public void execute() {
176             onSuggestionSelected(this);
177         }
178 
179         @Override
180         public boolean equals(Object obj) {
181             if (!(obj instanceof FilterSelectSuggestion)) {
182                 return false;
183             }
184             FilterSelectSuggestion other = (FilterSelectSuggestion) obj;
185             if ((key == null && other.key != null)
186                     || (key != null && !key.equals(other.key))) {
187                 return false;
188             }
189             if ((caption == null && other.caption != null)
190                     || (caption != null && !caption.equals(other.caption))) {
191                 return false;
192             }
193             if ((iconUri == null && other.iconUri != null)
194                     || (iconUri != null && !iconUri.equals(other.iconUri))) {
195                 return false;
196             }
197             return true;
198         }
199     }
200 
201     /**
202      * Represents the popup box with the selection options. Wraps a suggestion
203      * menu.
204      */
205     public class SuggestionPopup extends VOverlay implements PositionCallback,
206             CloseHandler<PopupPanel> {
207 
208         private static final int Z_INDEX = 30000;
209 
210         /** For internal use only. May be removed or replaced in the future. */
211         public final SuggestionMenu menu;
212 
213         private final Element up = DOM.createDiv();
214         private final Element down = DOM.createDiv();
215         private final Element status = DOM.createDiv();
216 
217         private boolean isPagingEnabled = true;
218 
219         private long lastAutoClosed;
220 
221         private int popupOuterPadding = -1;
222 
223         private int topPosition;
224 
225         /**
226          * Default constructor
227          */
228         SuggestionPopup() {
229             super(true, false, true);
230             debug("VFS.SP: constructor()");
231             setOwner(VFilterSelectPatched.this);
232             menu = new SuggestionMenu();
233             setWidget(menu);
234 
235             getElement().getStyle().setZIndex(Z_INDEX);
236 
237             final Element root = getContainerElement();
238 
239             up.setInnerHTML("<span>Prev</span>");
240             DOM.sinkEvents(up, Event.ONCLICK);
241 
242             down.setInnerHTML("<span>Next</span>");
243             DOM.sinkEvents(down, Event.ONCLICK);
244 
245             root.insertFirst(up);
246             root.appendChild(down);
247             root.appendChild(status);
248 
249             DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
250             addCloseHandler(this);
251 
252             Roles.getListRole().set(getElement());
253         }
254 
255         /**
256          * Shows the popup where the user can see the filtered options
257          * 
258          * @param currentSuggestions
259          *            The filtered suggestions
260          * @param currentPage
261          *            The current page number
262          * @param totalSuggestions
263          *            The total amount of suggestions
264          */
265         public void showSuggestions(
266                 final Collection<FilterSelectSuggestion> currentSuggestions,
267                 final int currentPage, final int totalSuggestions) {
268 
269             if (enableDebug) {
270                 debug("VFS.SP: showSuggestions(" + currentSuggestions + ", "
271                         + currentPage + ", " + totalSuggestions + ")");
272             }
273 
274             /*
275              * We need to defer the opening of the popup so that the parent DOM
276              * has stabilized so we can calculate an absolute top and left
277              * correctly. This issue manifests when a Combobox is placed in
278              * another popupView which also needs to calculate the absoluteTop()
279              * to position itself. #9768
280              */
281             final SuggestionPopup popup = this;
282             Scheduler.get().scheduleDeferred(new ScheduledCommand() {
283                 @Override
284                 public void execute() {
285                     // Add TT anchor point
286                     getElement().setId("VAADIN_COMBOBOX_OPTIONLIST");
287 
288                     menu.setSuggestions(currentSuggestions);
289                     final int x = VFilterSelectPatched.this.getAbsoluteLeft();
290 
291                     topPosition = tb.getAbsoluteTop();
292                     topPosition += tb.getOffsetHeight();
293 
294                     setPopupPosition(x, topPosition);
295 
296                     int nullOffset = (nullSelectionAllowed
297                             && "".equals(lastFilter) ? 1 : 0);
298                     boolean firstPage = (currentPage == 0);
299                     final int first = currentPage * pageLength + 1
300                             - (firstPage ? 0 : nullOffset);
301                     final int last = first
302                             + currentSuggestions.size()
303                             - 1
304                             - (firstPage && "".equals(lastFilter) ? nullOffset
305                                     : 0);
306                     final int matches = totalSuggestions - nullOffset;
307                     if (last > 0) {
308                         // nullsel not counted, as requested by user
309                         status.setInnerText((matches == 0 ? 0 : first) + "-"
310                                 + last + "/" + matches);
311                     } else {
312                         status.setInnerText("");
313                     }
314                     // We don't need to show arrows or statusbar if there is
315                     // only one
316                     // page
317                     if (totalSuggestions <= pageLength || pageLength == 0) {
318                         setPagingEnabled(false);
319                     } else {
320                         setPagingEnabled(true);
321                     }
322                     setPrevButtonActive(first > 1);
323                     setNextButtonActive(last < matches);
324 
325                     // clear previously fixed width
326                     menu.setWidth("");
327                     menu.getElement().getFirstChildElement().getStyle()
328                             .clearWidth();
329 
330                     setPopupPositionAndShow(popup);
331                 }
332             });
333         }
334 
335         /**
336          * Should the next page button be visible to the user?
337          * 
338          * @param active
339          */
340         private void setNextButtonActive(boolean active) {
341             if (enableDebug) {
342                 debug("VFS.SP: setNextButtonActive(" + active + ")");
343             }
344             if (active) {
345                 DOM.sinkEvents(down, Event.ONCLICK);
346                 down.setClassName(VFilterSelectPatched.this.getStylePrimaryName()
347                         + "-nextpage");
348             } else {
349                 DOM.sinkEvents(down, 0);
350                 down.setClassName(VFilterSelectPatched.this.getStylePrimaryName()
351                         + "-nextpage-off");
352             }
353         }
354 
355         /**
356          * Should the previous page button be visible to the user
357          * 
358          * @param active
359          */
360         private void setPrevButtonActive(boolean active) {
361             if (enableDebug) {
362                 debug("VFS.SP: setPrevButtonActive(" + active + ")");
363             }
364 
365             if (active) {
366                 DOM.sinkEvents(up, Event.ONCLICK);
367                 up.setClassName(VFilterSelectPatched.this.getStylePrimaryName()
368                         + "-prevpage");
369             } else {
370                 DOM.sinkEvents(up, 0);
371                 up.setClassName(VFilterSelectPatched.this.getStylePrimaryName()
372                         + "-prevpage-off");
373             }
374 
375         }
376 
377         /**
378          * Selects the next item in the filtered selections
379          */
380         public void selectNextItem() {
381             debug("VFS.SP: selectNextItem()");
382             final MenuItem cur = menu.getSelectedItem();
383             final int index = 1 + menu.getItems().indexOf(cur);
384             if (menu.getItems().size() > index) {
385                 final MenuItem newSelectedItem = menu.getItems().get(index);
386                 menu.selectItem(newSelectedItem);
387                 tb.setText(newSelectedItem.getText());
388                 tb.setSelectionRange(lastFilter.length(), newSelectedItem
389                         .getText().length() - lastFilter.length());
390 
391             } else if (hasNextPage()) {
392                 selectPopupItemWhenResponseIsReceived = Select.FIRST;
393                 filterOptions(currentPage + 1, lastFilter);
394             }
395         }
396 
397         /**
398          * Selects the previous item in the filtered selections
399          */
400         public void selectPrevItem() {
401             debug("VFS.SP: selectPrevItem()");
402             final MenuItem cur = menu.getSelectedItem();
403             final int index = -1 + menu.getItems().indexOf(cur);
404             if (index > -1) {
405                 final MenuItem newSelectedItem = menu.getItems().get(index);
406                 menu.selectItem(newSelectedItem);
407                 tb.setText(newSelectedItem.getText());
408                 tb.setSelectionRange(lastFilter.length(), newSelectedItem
409                         .getText().length() - lastFilter.length());
410             } else if (index == -1) {
411                 if (currentPage > 0) {
412                     selectPopupItemWhenResponseIsReceived = Select.LAST;
413                     filterOptions(currentPage - 1, lastFilter);
414                 }
415             } else {
416                 final MenuItem newSelectedItem = menu.getItems().get(
417                         menu.getItems().size() - 1);
418                 menu.selectItem(newSelectedItem);
419                 tb.setText(newSelectedItem.getText());
420                 tb.setSelectionRange(lastFilter.length(), newSelectedItem
421                         .getText().length() - lastFilter.length());
422             }
423         }
424 
425         /*
426          * Using a timer to scroll up or down the pages so when we receive lots
427          * of consecutive mouse wheel events the pages does not flicker.
428          */
429         private LazyPageScroller lazyPageScroller = new LazyPageScroller();
430 
431         private class LazyPageScroller extends Timer {
432             private int pagesToScroll = 0;
433 
434             @Override
435             public void run() {
436                 debug("VFS.SP.LPS: run()");
437                 if (pagesToScroll != 0) {
438                     if (!waitingForFilteringResponse) {
439                         /*
440                          * Avoid scrolling while we are waiting for a response
441                          * because otherwise the waiting flag will be reset in
442                          * the first response and the second response will be
443                          * ignored, causing an empty popup...
444                          * 
445                          * As long as the scrolling delay is suitable
446                          * double/triple clicks will work by scrolling two or
447                          * three pages at a time and this should not be a
448                          * problem.
449                          */
450                         filterOptions(currentPage + pagesToScroll, lastFilter);
451                     }
452                     pagesToScroll = 0;
453                 }
454             }
455 
456             public void scrollUp() {
457                 debug("VFS.SP.LPS: scrollUp()");
458                 if (pageLength > 0 && currentPage + pagesToScroll > 0) {
459                     pagesToScroll--;
460                     cancel();
461                     schedule(200);
462                 }
463             }
464 
465             public void scrollDown() {
466                 debug("VFS.SP.LPS: scrollDown()");
467                 if (pageLength > 0
468                         && totalMatches > (currentPage + pagesToScroll + 1)
469                         * pageLength) {
470                     pagesToScroll++;
471                     cancel();
472                     schedule(200);
473                 }
474             }
475         }
476 
477         /*
478          * (non-Javadoc)
479          * 
480          * @see
481          * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
482          * .user.client.Event)
483          */
484 
485         @Override
486         public void onBrowserEvent(Event event) {
487             debug("VFS.SP: onBrowserEvent()");
488             if (event.getTypeInt() == Event.ONCLICK) {
489                 final Element target = DOM.eventGetTarget(event);
490                 if (target == up || target == DOM.getChild(up, 0)) {
491                     lazyPageScroller.scrollUp();
492                 } else if (target == down || target == DOM.getChild(down, 0)) {
493                     lazyPageScroller.scrollDown();
494                 }
495             } else if (event.getTypeInt() == Event.ONMOUSEWHEEL) {
496                 int velocity = event.getMouseWheelVelocityY();
497                 if (velocity > 0) {
498                     lazyPageScroller.scrollDown();
499                 } else {
500                     lazyPageScroller.scrollUp();
501                 }
502             }
503 
504             /*
505              * Prevent the keyboard focus from leaving the textfield by
506              * preventing the default behaviour of the browser. Fixes #4285.
507              */
508             handleMouseDownEvent(event);
509         }
510 
511         /**
512          * Should paging be enabled. If paging is enabled then only a certain
513          * amount of items are visible at a time and a scrollbar or buttons are
514          * visible to change page. If paging is turned of then all options are
515          * rendered into the popup menu.
516          * 
517          * @param paging
518          *            Should the paging be turned on?
519          */
520         public void setPagingEnabled(boolean paging) {
521             debug("VFS.SP: setPagingEnabled(" + paging + ")");
522             if (isPagingEnabled == paging) {
523                 return;
524             }
525             if (paging) {
526                 down.getStyle().clearDisplay();
527                 up.getStyle().clearDisplay();
528                 status.getStyle().clearDisplay();
529             } else {
530                 down.getStyle().setDisplay(Display.NONE);
531                 up.getStyle().setDisplay(Display.NONE);
532                 status.getStyle().setDisplay(Display.NONE);
533             }
534             isPagingEnabled = paging;
535         }
536 
537         /*
538          * (non-Javadoc)
539          * 
540          * @see
541          * com.google.gwt.user.client.ui.PopupPanel$PositionCallback#setPosition
542          * (int, int)
543          */
544 
545         @Override
546         public void setPosition(int offsetWidth, int offsetHeight) {
547             debug("VFS.SP: setPosition()");
548 
549             int top = topPosition;
550             int left = getPopupLeft();
551 
552             // reset menu size and retrieve its "natural" size
553             menu.setHeight("");
554             if (currentPage > 0 && !hasNextPage()) {
555                 // fix height to avoid height change when getting to last page
556                 menu.fixHeightTo(pageLength);
557             }
558 
559             final int desiredHeight = offsetHeight = getOffsetHeight();
560             final int desiredWidth = getMainWidth();
561 
562             debug("VFS.SP:     desired[" + desiredWidth + ", " + desiredHeight
563                     + "]");
564 
565             Element menuFirstChild = menu.getElement().getFirstChildElement().cast();
566             final int naturalMenuWidth = menuFirstChild.getOffsetWidth();
567 
568             if (popupOuterPadding == -1) {
569                 popupOuterPadding = Util
570                         .measureHorizontalPaddingAndBorder(getElement(), 2);
571             }
572 
573             if (naturalMenuWidth < desiredWidth) {
574                 menu.setWidth((desiredWidth - popupOuterPadding) + "px");
575                 menuFirstChild.getStyle().setWidth(100, Unit.PCT);
576             }
577 
578             if (BrowserInfo.get().isIE()) {
579                 /*
580                  * IE requires us to specify the width for the container
581                  * element. Otherwise it will be 100% wide
582                  */
583                 int rootWidth = Math.max(desiredWidth, naturalMenuWidth)
584                         - popupOuterPadding;
585                 getContainerElement().getStyle().setWidth(rootWidth, Unit.PX);
586             }
587 
588             final int vfsHeight = VFilterSelectPatched.this.getOffsetHeight();
589             final int spaceAvailableAbove = top - vfsHeight;
590             final int spaceAvailableBelow = Window.getClientHeight() - top;
591             if (spaceAvailableBelow < offsetHeight
592                     && spaceAvailableBelow < spaceAvailableAbove) {
593                 // popup on top of input instead
594                 top -= offsetHeight + vfsHeight;
595                 if (top < 0) {
596                     offsetHeight += top;
597                     top = 0;
598                 }
599             } else {
600                 offsetHeight = Math.min(offsetHeight, spaceAvailableBelow);
601             }
602 
603             // fetch real width (mac FF bugs here due GWT popups overflow:auto )
604             offsetWidth = menuFirstChild.getOffsetWidth();
605 
606             if (offsetHeight < desiredHeight) {
607                 int menuHeight = offsetHeight;
608                 if (isPagingEnabled) {
609                     menuHeight -= up.getOffsetHeight() + down.getOffsetHeight()
610                             + status.getOffsetHeight();
611                 } else {
612                     final ComputedStyle s = new ComputedStyle(menu.getElement());
613                     menuHeight -= s.getIntProperty("marginBottom")
614                             + s.getIntProperty("marginTop");
615                 }
616                 // MGNLUI-3685 avoid negative size, else menu.setHeight(..)
617                 // below will throw an exception. Give a sensible default height.
618                 if (menuHeight <= 0) {
619                     menuHeight = 100;
620                 }
621 
622                 menu.setHeight(menuHeight + "px");
623 
624                 final int naturalMenuWidthPlusScrollBar = naturalMenuWidth
625                         + Util.getNativeScrollbarSize();
626                 if (offsetWidth < naturalMenuWidthPlusScrollBar) {
627                     menu.setWidth(naturalMenuWidthPlusScrollBar + "px");
628                 }
629             }
630 
631             if (offsetWidth + left > Window.getClientWidth()) {
632                 left = VFilterSelectPatched.this.getAbsoluteLeft()
633                         + VFilterSelectPatched.this.getOffsetWidth() - offsetWidth;
634                 if (left < 0) {
635                     left = 0;
636                     menu.setWidth(Window.getClientWidth() + "px");
637                 }
638             }
639 
640             setPopupPosition(left, top);
641             menu.scrollSelectionIntoView();
642         }
643 
644         /**
645          * Was the popup just closed?
646          * 
647          * @return true if popup was just closed
648          */
649         public boolean isJustClosed() {
650             debug("VFS.SP: justClosed()");
651             final long now = (new Date()).getTime();
652             return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
653         }
654 
655         /*
656          * (non-Javadoc)
657          * 
658          * @see
659          * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google
660          * .gwt.event.logical.shared.CloseEvent)
661          */
662 
663         @Override
664         public void onClose(CloseEvent<PopupPanel> event) {
665             if (enableDebug) {
666                 debug("VFS.SP: onClose(" + event.isAutoClosed() + ")");
667             }
668             if (event.isAutoClosed()) {
669                 lastAutoClosed = (new Date()).getTime();
670             }
671         }
672 
673         /**
674          * Updates style names in suggestion popup to help theme building.
675          * 
676          * @param uidl
677          *            UIDL for the whole combo box
678          * @param componentState
679          *            shared state of the combo box
680          */
681         public void updateStyleNames(UIDL uidl,
682                 AbstractComponentState componentState) {
683             debug("VFS.SP: updateStyleNames()");
684             setStyleName(VFilterSelectPatched.this.getStylePrimaryName()
685                     + "-suggestpopup");
686             menu.setStyleName(VFilterSelectPatched.this.getStylePrimaryName()
687                     + "-suggestmenu");
688             status.setClassName(VFilterSelectPatched.this.getStylePrimaryName()
689                     + "-status");
690             if (ComponentStateUtil.hasStyles(componentState)) {
691                 for (String style : componentState.styles) {
692                     if (!"".equals(style)) {
693                         addStyleDependentName(style);
694                     }
695                 }
696             }
697         }
698 
699     }
700 
701     /**
702      * The menu where the suggestions are rendered
703      */
704     public class SuggestionMenu extends MenuBar implements SubPartAware,
705             LoadHandler {
706 
707         /**
708          * Tracks the item that is currently selected using the keyboard. This
709          * is need only because mouseover changes the selection and we do not
710          * want to use that selection when pressing enter to select the item.
711          */
712         private MenuItem keyboardSelectedItem;
713 
714         private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(
715                 100, new ScheduledCommand() {
716 
717                     @Override
718                     public void execute() {
719                         debug("VFS.SM: delayedImageLoadExecutioner()");
720                         if (suggestionPopup.isVisible()
721                                 && suggestionPopup.isAttached()) {
722                             setWidth("");
723                             getElement().getFirstChildElement().getStyle()
724                                     .clearWidth();
725                             suggestionPopup
726                                     .setPopupPositionAndShow(suggestionPopup);
727                         }
728 
729                     }
730                 });
731 
732         /**
733          * Default constructor
734          */
735         SuggestionMenu() {
736             super(true);
737             debug("VFS.SM: constructor()");
738             addDomHandler(this, LoadEvent.getType());
739             DOM.setStyleAttribute(getElement(), "overflowY", Style.Overflow.AUTO.getCssName());
740         }
741 
742         /**
743          * This and the method below are part of Vaadin's MenuBar since 7.2.6 but was put directly here
744          * as otherwise that would have meant patching not only MenuBar but also
745          * MenuItem and MenuBarConnector.
746          */
747         private void scrollItemIntoView(MenuItem item) {
748             if (item != null) {
749                 item.getElement().scrollIntoView();
750             }
751         }
752 
753         public void scrollSelectionIntoView() {
754             scrollItemIntoView(getSelectedItem());
755         }
756 
757         /**
758          * Fixes menus height to use same space as full page would use. Needed
759          * to avoid height changes when quickly "scrolling" to last page
760          */
761         public void fixHeightTo(int pagelenth) {
762             if (currentSuggestions.size() > 0) {
763                 final int pixels = pagelenth * (getOffsetHeight() - 2)
764                         / currentSuggestions.size();
765                 setHeight((pixels + 2) + "px");
766             }
767         }
768 
769         /**
770          * Sets the suggestions rendered in the menu
771          * 
772          * @param suggestions
773          *            The suggestions to be rendered in the menu
774          */
775         public void setSuggestions(
776                 Collection<FilterSelectSuggestion> suggestions) {
777             if (enableDebug) {
778                 debug("VFS.SM: setSuggestions(" + suggestions + ")");
779             }
780             // Reset keyboard selection when contents is updated to avoid
781             // reusing old, invalid data
782             setKeyboardSelectedItem(null);
783 
784             clearItems();
785             final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
786             while (it.hasNext()) {
787                 final FilterSelectSuggestion s = it.next();
788                 final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
789                 Roles.getListitemRole().set(mi.getElement());
790 
791                 Util.sinkOnloadForImages(mi.getElement());
792 
793                 this.addItem(mi);
794                 if (s == currentSuggestion) {
795                     selectItem(mi);
796                 }
797             }
798         }
799 
800         /**
801          * Send the current selection to the server. Triggered when a selection
802          * is made or on a blur event.
803          */
804         public void doSelectedItemAction() {
805             debug("VFS.SM: doSelectedItemAction()");
806             // do not send a value change event if null was and stays selected
807             final String enteredItemValue = tb.getText();
808             if (nullSelectionAllowed && "".equals(enteredItemValue)
809                     && selectedOptionKey != null
810                     && !"".equals(selectedOptionKey)) {
811                 if (nullSelectItem) {
812                     reset();
813                     return;
814                 }
815                 // null is not visible on pages != 0, and not visible when
816                 // filtering: handle separately
817                 client.updateVariable(paintableId, "filter", "", false);
818                 client.updateVariable(paintableId, "page", 0, false);
819                 client.updateVariable(paintableId, "selected", new String[] {},
820                         immediate);
821                 suggestionPopup.hide();
822                 return;
823             }
824 
825             updateSelectionWhenReponseIsReceived = waitingForFilteringResponse;
826             if (!waitingForFilteringResponse) {
827                 doPostFilterSelectedItemAction();
828             }
829         }
830 
831         /**
832          * Triggered after a selection has been made
833          */
834         public void doPostFilterSelectedItemAction() {
835             debug("VFS.SM: doPostFilterSelectedItemAction()");
836             final MenuItem item = getSelectedItem();
837             final String enteredItemValue = tb.getText();
838 
839             updateSelectionWhenReponseIsReceived = false;
840 
841             // check for exact match in menu
842             int p = getItems().size();
843             if (p > 0) {
844                 for (int i = 0; i < p; i++) {
845                     final MenuItem potentialExactMatch = getItems().get(i);
846                     if (potentialExactMatch.getText().equals(enteredItemValue)) {
847                         selectItem(potentialExactMatch);
848                         // do not send a value change event if null was and
849                         // stays selected
850                         if (!"".equals(enteredItemValue)
851                                 || (selectedOptionKey != null && !""
852                                         .equals(selectedOptionKey))) {
853                             doItemAction(potentialExactMatch, true);
854                         }
855                         suggestionPopup.hide();
856                         return;
857                     }
858                 }
859             }
860             if (allowNewItem) {
861 
862                 if (!prompting && !enteredItemValue.equals(lastNewItemString)) {
863                     /*
864                      * Store last sent new item string to avoid double sends
865                      */
866                     lastNewItemString = enteredItemValue;
867                     client.updateVariable(paintableId, "newitem",
868                             enteredItemValue, immediate);
869                 }
870             } else if (item != null
871                     && !"".equals(lastFilter)
872                     && (filteringmode == FilteringMode.CONTAINS ? item
873                             .getText().toLowerCase()
874                             .contains(lastFilter.toLowerCase()) : item
875                             .getText().toLowerCase()
876                             .startsWith(lastFilter.toLowerCase()))) {
877                 doItemAction(item, true);
878             } else {
879                 // currentSuggestion has key="" for nullselection
880                 if (currentSuggestion != null
881                         && !currentSuggestion.key.equals("")) {
882                     // An item (not null) selected
883                     String text = currentSuggestion.getReplacementString();
884                     tb.setText(text);
885                     selectedOptionKey = currentSuggestion.key;
886                 } else {
887                     // Null selected
888                     tb.setText("");
889                     selectedOptionKey = null;
890                 }
891             }
892             suggestionPopup.hide();
893         }
894 
895         private static final String SUBPART_PREFIX = "item";
896 
897         @Override
898         public Element getSubPartElement(String subPart) {
899             int index = Integer.parseInt(subPart.substring(SUBPART_PREFIX
900                     .length()));
901 
902             MenuItem item = getItems().get(index);
903 
904             return item.getElement();
905         }
906 
907         @Override
908         public String getSubPartName(Element subElement) {
909             if (!getElement().isOrHasChild(subElement)) {
910                 return null;
911             }
912 
913             Element menuItemRoot = subElement;
914             while (menuItemRoot != null
915                     && !menuItemRoot.getTagName().equalsIgnoreCase("td")) {
916                 menuItemRoot = menuItemRoot.getParentElement().cast();
917             }
918             // "menuItemRoot" is now the root of the menu item
919 
920             final int itemCount = getItems().size();
921             for (int i = 0; i < itemCount; i++) {
922                 if (getItems().get(i).getElement() == menuItemRoot) {
923                     String name = SUBPART_PREFIX + i;
924                     return name;
925                 }
926             }
927             return null;
928         }
929 
930         @Override
931         public void onLoad(LoadEvent event) {
932             debug("VFS.SM: onLoad()");
933             // Handle icon onload events to ensure shadow is resized
934             // correctly
935             delayedImageLoadExecutioner.trigger();
936 
937         }
938 
939         public void selectFirstItem() {
940             debug("VFS.SM: selectFirstItem()");
941             MenuItem firstItem = getItems().get(0);
942             selectItem(firstItem);
943         }
944 
945         private MenuItem getKeyboardSelectedItem() {
946             return keyboardSelectedItem;
947         }
948 
949         public void setKeyboardSelectedItem(MenuItem firstItem) {
950             keyboardSelectedItem = firstItem;
951         }
952 
953         public void selectLastItem() {
954             debug("VFS.SM: selectLastItem()");
955             List<MenuItem> items = getItems();
956             MenuItem lastItem = items.get(items.size() - 1);
957             selectItem(lastItem);
958         }
959     }
960 
961     /**
962      * TextBox variant used as input element for filter selects, which prevents
963      * selecting text when disabled.
964      * 
965      * @since 7.1.5
966      */
967     public class FilterSelectTextBox extends TextBox {
968 
969         /**
970          * Overridden to avoid selecting text when text input is disabled
971          */
972         @Override
973         public void setSelectionRange(int pos, int length) {
974             if (textInputEnabled) {
975                 super.setSelectionRange(pos, length);
976             } else {
977                 super.setSelectionRange(getValue().length(), 0);
978             }
979         }
980     }
981 
982     @Deprecated
983     public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF;
984     @Deprecated
985     public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH;
986     @Deprecated
987     public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS;
988 
989     public static final String CLASSNAME = "v-filterselect";
990     private static final String STYLE_NO_INPUT = "no-input";
991 
992     /** For internal use only. May be removed or replaced in the future. */
993     public int pageLength = 10;
994 
995     private boolean enableDebug = false;
996 
997     private final FlowPanel panel = new FlowPanel();
998 
999     /**
1000      * The text box where the filter is written
1001      * <p>
1002      * For internal use only. May be removed or replaced in the future.
1003      */
1004     public final TextBox tb;
1005 
1006     /** For internal use only. May be removed or replaced in the future. */
1007     public final SuggestionPopup suggestionPopup;
1008 
1009     /**
1010      * Used when measuring the width of the popup
1011      */
1012     private final HTML popupOpener = new HTML("") {
1013 
1014         /*
1015          * (non-Javadoc)
1016          * 
1017          * @see
1018          * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
1019          * .user.client.Event)
1020          */
1021 
1022         @Override
1023         public void onBrowserEvent(Event event) {
1024             super.onBrowserEvent(event);
1025 
1026             /*
1027              * Prevent the keyboard focus from leaving the textfield by
1028              * preventing the default behaviour of the browser. Fixes #4285.
1029              */
1030             handleMouseDownEvent(event);
1031         }
1032     };
1033 
1034     private final Image selectedItemIcon = new Image();
1035 
1036     /** For internal use only. May be removed or replaced in the future. */
1037     public ApplicationConnection client;
1038 
1039     /** For internal use only. May be removed or replaced in the future. */
1040     public String paintableId;
1041 
1042     /** For internal use only. May be removed or replaced in the future. */
1043     public int currentPage;
1044 
1045     /**
1046      * A collection of available suggestions (options) as received from the
1047      * server.
1048      * <p>
1049      * For internal use only. May be removed or replaced in the future.
1050      */
1051     public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
1052 
1053     /** For internal use only. May be removed or replaced in the future. */
1054     public boolean immediate;
1055 
1056     /** For internal use only. May be removed or replaced in the future. */
1057     public String selectedOptionKey;
1058 
1059     /** For internal use only. May be removed or replaced in the future. */
1060     public boolean waitingForFilteringResponse = false;
1061 
1062     /** For internal use only. May be removed or replaced in the future. */
1063     public boolean updateSelectionWhenReponseIsReceived = false;
1064 
1065     private boolean tabPressedWhenPopupOpen = false;
1066 
1067     /** For internal use only. May be removed or replaced in the future. */
1068     public boolean initDone = false;
1069 
1070     /** For internal use only. May be removed or replaced in the future. */
1071     public String lastFilter = "";
1072 
1073     /** For internal use only. May be removed or replaced in the future. */
1074     public enum Select {
1075         NONE, FIRST, LAST
1076     };
1077 
1078     /** For internal use only. May be removed or replaced in the future. */
1079     public Select selectPopupItemWhenResponseIsReceived = Select.NONE;
1080 
1081     /**
1082      * The current suggestion selected from the dropdown. This is one of the
1083      * values in currentSuggestions except when filtering, in this case
1084      * currentSuggestion might not be in currentSuggestions.
1085      * <p>
1086      * For internal use only. May be removed or replaced in the future.
1087      */
1088     public FilterSelectSuggestion currentSuggestion;
1089 
1090     /** For internal use only. May be removed or replaced in the future. */
1091     public boolean allowNewItem;
1092 
1093     /** For internal use only. May be removed or replaced in the future. */
1094     public int totalMatches;
1095 
1096     /** For internal use only. May be removed or replaced in the future. */
1097     public boolean nullSelectionAllowed;
1098 
1099     /** For internal use only. May be removed or replaced in the future. */
1100     public boolean nullSelectItem;
1101 
1102     /** For internal use only. May be removed or replaced in the future. */
1103     public boolean enabled;
1104 
1105     /** For internal use only. May be removed or replaced in the future. */
1106     public boolean readonly;
1107 
1108     /** For internal use only. May be removed or replaced in the future. */
1109     public FilteringMode filteringmode = FilteringMode.OFF;
1110 
1111     // shown in unfocused empty field, disappears on focus (e.g "Search here")
1112     private static final String CLASSNAME_PROMPT = "prompt";
1113 
1114     /** For internal use only. May be removed or replaced in the future. */
1115     public String inputPrompt = "";
1116 
1117     /** For internal use only. May be removed or replaced in the future. */
1118     public boolean prompting = false;
1119 
1120     /**
1121      * Set true when popupopened has been clicked. Cleared on each UIDL-update.
1122      * This handles the special case where are not filtering yet and the
1123      * selected value has changed on the server-side. See #2119
1124      * <p>
1125      * For internal use only. May be removed or replaced in the future.
1126      */
1127     public boolean popupOpenerClicked;
1128 
1129     /** For internal use only. May be removed or replaced in the future. */
1130     public int suggestionPopupMinWidth = 0;
1131 
1132     private int popupWidth = -1;
1133     /**
1134      * Stores the last new item string to avoid double submissions. Cleared on
1135      * uidl updates.
1136      * <p>
1137      * For internal use only. May be removed or replaced in the future.
1138      */
1139     public String lastNewItemString;
1140 
1141     /** For internal use only. May be removed or replaced in the future. */
1142     public boolean focused = false;
1143 
1144     /**
1145      * If set to false, the component should not allow entering text to the
1146      * field even for filtering.
1147      */
1148     private boolean textInputEnabled = true;
1149 
1150     /**
1151      * Default constructor.
1152      */
1153     public VFilterSelectPatched() {
1154         tb = createTextBox();
1155         suggestionPopup = createSuggestionPopup();
1156 
1157         selectedItemIcon.setStyleName("v-icon");
1158         selectedItemIcon.addLoadHandler(new LoadHandler() {
1159 
1160             @Override
1161             public void onLoad(LoadEvent event) {
1162                 if (BrowserInfo.get().isIE8()) {
1163                     // IE8 needs some help to discover it should reposition the
1164                     // text field
1165                     forceReflow();
1166                 }
1167                 updateRootWidth();
1168                 updateSelectedIconPosition();
1169             }
1170         });
1171 
1172         popupOpener.sinkEvents(Event.ONMOUSEDOWN);
1173         Roles.getButtonRole()
1174                 .setAriaHiddenState(popupOpener.getElement(), true);
1175         Roles.getButtonRole().set(popupOpener.getElement());
1176 
1177         panel.add(tb);
1178         panel.add(popupOpener);
1179         initWidget(panel);
1180         Roles.getComboboxRole().set(panel.getElement());
1181 
1182         tb.addKeyDownHandler(this);
1183         tb.addKeyUpHandler(this);
1184 
1185         tb.addFocusHandler(this);
1186         tb.addBlurHandler(this);
1187         tb.addClickHandler(this);
1188 
1189         popupOpener.addClickHandler(this);
1190 
1191         setStyleName(CLASSNAME);
1192     }
1193 
1194     /**
1195      * This method will create the TextBox used by the VFilterSelect instance.
1196      * It is invoked during the Constructor and should only be overridden if a
1197      * custom TextBox shall be used. The overriding method cannot use any
1198      * instance variables.
1199      * 
1200      * @since 7.1.5
1201      * @return TextBox instance used by this VFilterSelect
1202      */
1203     protected TextBox createTextBox() {
1204         return new FilterSelectTextBox();
1205     }
1206 
1207     /**
1208      * This method will create the SuggestionPopup used by the VFilterSelect
1209      * instance. It is invoked during the Constructor and should only be
1210      * overridden if a custom SuggestionPopup shall be used. The overriding
1211      * method cannot use any instance variables.
1212      * 
1213      * @since 7.1.5
1214      * @return SuggestionPopup instance used by this VFilterSelect
1215      */
1216     protected SuggestionPopup createSuggestionPopup() {
1217         return new SuggestionPopup();
1218     }
1219 
1220     @Override
1221     public void setStyleName(String style) {
1222         super.setStyleName(style);
1223         updateStyleNames();
1224     }
1225 
1226     @Override
1227     public void setStylePrimaryName(String style) {
1228         super.setStylePrimaryName(style);
1229         updateStyleNames();
1230     }
1231 
1232     protected void updateStyleNames() {
1233         tb.setStyleName(getStylePrimaryName() + "-input");
1234         popupOpener.setStyleName(getStylePrimaryName() + "-button");
1235         suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup");
1236     }
1237 
1238     /**
1239      * Does the Select have more pages?
1240      * 
1241      * @return true if a next page exists, else false if the current page is the
1242      *         last page
1243      */
1244     public boolean hasNextPage() {
1245         if (totalMatches > (currentPage + 1) * pageLength) {
1246             return true;
1247         } else {
1248             return false;
1249         }
1250     }
1251 
1252     /**
1253      * Filters the options at a certain page. Uses the text box input as a
1254      * filter
1255      * 
1256      * @param page
1257      *            The page which items are to be filtered
1258      */
1259     public void filterOptions(int page) {
1260         filterOptions(page, tb.getText());
1261     }
1262 
1263     /**
1264      * Filters the options at certain page using the given filter
1265      * 
1266      * @param page
1267      *            The page to filter
1268      * @param filter
1269      *            The filter to apply to the components
1270      */
1271     public void filterOptions(int page, String filter) {
1272         filterOptions(page, filter, true);
1273     }
1274 
1275     /**
1276      * Filters the options at certain page using the given filter
1277      * 
1278      * @param page
1279      *            The page to filter
1280      * @param filter
1281      *            The filter to apply to the options
1282      * @param immediate
1283      *            Whether to send the options request immediately
1284      */
1285     private void filterOptions(int page, String filter, boolean immediate) {
1286         if (enableDebug) {
1287             debug("VFS: filterOptions(" + page + ", " + filter + ", "
1288                     + immediate + ")");
1289         }
1290         if (filter.equals(lastFilter) && currentPage == page) {
1291             if (!suggestionPopup.isAttached()) {
1292                 suggestionPopup.showSuggestions(currentSuggestions,
1293                         currentPage, totalMatches);
1294             }
1295             return;
1296         }
1297         if (!filter.equals(lastFilter)) {
1298             // we are on subsequent page and text has changed -> reset page
1299             if ("".equals(filter)) {
1300                 // let server decide
1301                 page = -1;
1302             } else {
1303                 page = 0;
1304             }
1305         }
1306 
1307         waitingForFilteringResponse = true;
1308         client.updateVariable(paintableId, "filter", filter, false);
1309         client.updateVariable(paintableId, "page", page, immediate);
1310         lastFilter = filter;
1311         currentPage = page;
1312     }
1313 
1314     /** For internal use only. May be removed or replaced in the future. */
1315     public void updateReadOnly() {
1316         debug("VFS: updateReadOnly()");
1317         tb.setReadOnly(readonly || !textInputEnabled);
1318     }
1319 
1320     public void setTextInputEnabled(boolean textInputEnabled) {
1321         debug("VFS: setTextInputEnabled()");
1322         // Always update styles as they might have been overwritten
1323         if (textInputEnabled) {
1324             removeStyleDependentName(STYLE_NO_INPUT);
1325             Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement());
1326         } else {
1327             addStyleDependentName(STYLE_NO_INPUT);
1328             Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(),
1329                     true);
1330         }
1331 
1332         if (this.textInputEnabled == textInputEnabled) {
1333             return;
1334         }
1335 
1336         this.textInputEnabled = textInputEnabled;
1337         updateReadOnly();
1338     }
1339 
1340     /**
1341      * Sets the text in the text box.
1342      * 
1343      * @param text
1344      *            the text to set in the text box
1345      */
1346     public void setTextboxText(final String text) {
1347         if (enableDebug) {
1348             debug("VFS: setTextboxText(" + text + ")");
1349         }
1350         tb.setText(text);
1351     }
1352 
1353     /**
1354      * Turns prompting on. When prompting is turned on a command prompt is shown
1355      * in the text box if nothing has been entered.
1356      */
1357     public void setPromptingOn() {
1358         debug("VFS: setPromptingOn()");
1359         if (!prompting) {
1360             prompting = true;
1361             addStyleDependentName(CLASSNAME_PROMPT);
1362         }
1363         setTextboxText(inputPrompt);
1364     }
1365 
1366     /**
1367      * Turns prompting off. When prompting is turned on a command prompt is
1368      * shown in the text box if nothing has been entered.
1369      * <p>
1370      * For internal use only. May be removed or replaced in the future.
1371      * 
1372      * @param text
1373      *            The text the text box should contain.
1374      */
1375     public void setPromptingOff(String text) {
1376         debug("VFS: setPromptingOff()");
1377         setTextboxText(text);
1378         if (prompting) {
1379             prompting = false;
1380             removeStyleDependentName(CLASSNAME_PROMPT);
1381         }
1382     }
1383 
1384     /**
1385      * Triggered when a suggestion is selected
1386      * 
1387      * @param suggestion
1388      *            The suggestion that just got selected.
1389      */
1390     public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
1391         if (enableDebug) {
1392             debug("VFS: onSuggestionSelected(" + suggestion.caption + ": "
1393                     + suggestion.key + ")");
1394         }
1395         updateSelectionWhenReponseIsReceived = false;
1396 
1397         currentSuggestion = suggestion;
1398         String newKey;
1399         if (suggestion.key.equals("")) {
1400             // "nullselection"
1401             newKey = "";
1402         } else {
1403             // normal selection
1404             newKey = suggestion.getOptionKey();
1405         }
1406 
1407         String text = suggestion.getReplacementString();
1408         if ("".equals(newKey) && !focused) {
1409             setPromptingOn();
1410         } else {
1411             setPromptingOff(text);
1412         }
1413         setSelectedItemIcon(suggestion.getIconUri());
1414         if (!(newKey.equals(selectedOptionKey) || ("".equals(newKey) && selectedOptionKey == null))) {
1415             selectedOptionKey = newKey;
1416             client.updateVariable(paintableId, "selected",
1417                     new String[] { selectedOptionKey }, immediate);
1418             // currentPage = -1; // forget the page
1419         }
1420         suggestionPopup.hide();
1421     }
1422 
1423     /**
1424      * Sets the icon URI of the selected item. The icon is shown on the left
1425      * side of the item caption text. Set the URI to null to remove the icon.
1426      * 
1427      * @param iconUri
1428      *            The URI of the icon
1429      */
1430     public void setSelectedItemIcon(String iconUri) {
1431         if (iconUri == null || iconUri.length() == 0) {
1432             if (selectedItemIcon.isAttached()) {
1433                 panel.remove(selectedItemIcon);
1434                 if (BrowserInfo.get().isIE8()) {
1435                     // IE8 needs some help to discover it should reposition the
1436                     // text field
1437                     forceReflow();
1438                 }
1439                 updateRootWidth();
1440             }
1441         } else {
1442             panel.insert(selectedItemIcon, 0);
1443             selectedItemIcon.setUrl(iconUri);
1444             updateRootWidth();
1445             updateSelectedIconPosition();
1446         }
1447     }
1448 
1449     private void forceReflow() {
1450         Util.setStyleTemporarily(tb.getElement(), "zoom", "1");
1451     }
1452 
1453     /**
1454      * Positions the icon vertically in the middle. Should be called after the
1455      * icon has loaded
1456      */
1457     private void updateSelectedIconPosition() {
1458         // Position icon vertically to middle
1459         int availableHeight = 0;
1460         availableHeight = getOffsetHeight();
1461 
1462         int iconHeight = Util.getRequiredHeight(selectedItemIcon);
1463         int marginTop = (availableHeight - iconHeight) / 2;
1464         DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop",
1465                 marginTop + "px");
1466     }
1467 
1468     private static Set<Integer> navigationKeyCodes = new HashSet<Integer>();
1469     static {
1470         navigationKeyCodes.add(KeyCodes.KEY_DOWN);
1471         navigationKeyCodes.add(KeyCodes.KEY_UP);
1472         navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN);
1473         navigationKeyCodes.add(KeyCodes.KEY_PAGEUP);
1474         navigationKeyCodes.add(KeyCodes.KEY_ENTER);
1475     }
1476 
1477     /*
1478      * (non-Javadoc)
1479      * 
1480      * @see
1481      * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
1482      * .event.dom.client.KeyDownEvent)
1483      */
1484 
1485     @Override
1486     public void onKeyDown(KeyDownEvent event) {
1487         if (enabled && !readonly) {
1488             int keyCode = event.getNativeKeyCode();
1489 
1490             if (enableDebug) {
1491                 debug("VFS: key down: " + keyCode);
1492             }
1493             if (waitingForFilteringResponse
1494                     && navigationKeyCodes.contains(keyCode)) {
1495                 /*
1496                  * Keyboard navigation events should not be handled while we are
1497                  * waiting for a response. This avoids flickering, disappearing
1498                  * items, wrongly interpreted responses and more.
1499                  */
1500                 if (enableDebug) {
1501                     debug("Ignoring "
1502                             + keyCode
1503                             + " because we are waiting for a filtering response");
1504                 }
1505                 DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
1506                 event.stopPropagation();
1507                 return;
1508             }
1509 
1510             if (suggestionPopup.isAttached()) {
1511                 if (enableDebug) {
1512                     debug("Keycode " + keyCode + " target is popup");
1513                 }
1514                 popupKeyDown(event);
1515             } else {
1516                 if (enableDebug) {
1517                     debug("Keycode " + keyCode + " target is text field");
1518                 }
1519                 inputFieldKeyDown(event);
1520             }
1521         }
1522     }
1523 
1524     private void debug(String string) {
1525         if (enableDebug) {
1526             VConsole.error(string);
1527         }
1528     }
1529 
1530     /**
1531      * Triggered when a key is pressed in the text box
1532      * 
1533      * @param event
1534      *            The KeyDownEvent
1535      */
1536     private void inputFieldKeyDown(KeyDownEvent event) {
1537         if (enableDebug) {
1538             debug("VFS: inputFieldKeyDown(" + event.getNativeKeyCode() + ")");
1539         }
1540         switch (event.getNativeKeyCode()) {
1541         case KeyCodes.KEY_DOWN:
1542         case KeyCodes.KEY_UP:
1543         case KeyCodes.KEY_PAGEDOWN:
1544         case KeyCodes.KEY_PAGEUP:
1545             // open popup as from gadget
1546             filterOptions(-1, "");
1547             lastFilter = "";
1548             tb.selectAll();
1549             break;
1550         case KeyCodes.KEY_ENTER:
1551             /*
1552              * This only handles the case when new items is allowed, a text is
1553              * entered, the popup opener button is clicked to close the popup
1554              * and enter is then pressed (see #7560).
1555              */
1556             if (!allowNewItem) {
1557                 return;
1558             }
1559 
1560             if (currentSuggestion != null
1561                     && tb.getText().equals(
1562                             currentSuggestion.getReplacementString())) {
1563                 // Retain behavior from #6686 by returning without stopping
1564                 // propagation if there's nothing to do
1565                 return;
1566             }
1567             suggestionPopup.menu.doSelectedItemAction();
1568 
1569             event.stopPropagation();
1570             break;
1571         }
1572 
1573     }
1574 
1575     /**
1576      * Triggered when a key was pressed in the suggestion popup.
1577      * 
1578      * @param event
1579      *            The KeyDownEvent of the key
1580      */
1581     private void popupKeyDown(KeyDownEvent event) {
1582         if (enableDebug) {
1583             debug("VFS: popupKeyDown(" + event.getNativeKeyCode() + ")");
1584         }
1585         // Propagation of handled events is stopped so other handlers such as
1586         // shortcut key handlers do not also handle the same events.
1587         switch (event.getNativeKeyCode()) {
1588         case KeyCodes.KEY_DOWN:
1589             suggestionPopup.selectNextItem();
1590             suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu
1591                     .getSelectedItem());
1592             DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
1593             event.stopPropagation();
1594             break;
1595         case KeyCodes.KEY_UP:
1596             suggestionPopup.selectPrevItem();
1597             suggestionPopup.menu.setKeyboardSelectedItem(suggestionPopup.menu
1598                     .getSelectedItem());
1599             DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
1600             event.stopPropagation();
1601             break;
1602         case KeyCodes.KEY_PAGEDOWN:
1603             if (hasNextPage()) {
1604                 filterOptions(currentPage + 1, lastFilter);
1605             }
1606             event.stopPropagation();
1607             break;
1608         case KeyCodes.KEY_PAGEUP:
1609             if (currentPage > 0) {
1610                 filterOptions(currentPage - 1, lastFilter);
1611             }
1612             event.stopPropagation();
1613             break;
1614         case KeyCodes.KEY_TAB:
1615             tabPressedWhenPopupOpen = true;
1616             filterOptions(currentPage);
1617             // onBlur() takes care of the rest
1618             break;
1619         case KeyCodes.KEY_ESCAPE:
1620             reset();
1621             DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
1622             event.stopPropagation();
1623             break;
1624         case KeyCodes.KEY_ENTER:
1625             if (suggestionPopup.menu.getKeyboardSelectedItem() == null) {
1626                 /*
1627                  * Nothing selected using up/down. Happens e.g. when entering a
1628                  * text (causes popup to open) and then pressing enter.
1629                  */
1630                 if (!allowNewItem) {
1631                     /*
1632                      * New items are not allowed: If there is only one
1633                      * suggestion, select that. Otherwise do nothing.
1634                      */
1635                     if (currentSuggestions.size() == 1) {
1636                         onSuggestionSelected(currentSuggestions.get(0));
1637                     }
1638                 } else {
1639                     // Handle addition of new items.
1640                     suggestionPopup.menu.doSelectedItemAction();
1641                 }
1642             } else {
1643                 /*
1644                  * Get the suggestion that was navigated to using up/down.
1645                  */
1646                 currentSuggestion = ((FilterSelectSuggestion) suggestionPopup.menu
1647                         .getKeyboardSelectedItem().getCommand());
1648                 onSuggestionSelected(currentSuggestion);
1649             }
1650 
1651             event.stopPropagation();
1652             break;
1653         }
1654 
1655     }
1656 
1657     /**
1658      * Triggered when a key was depressed
1659      * 
1660      * @param event
1661      *            The KeyUpEvent of the key depressed
1662      */
1663 
1664     @Override
1665     public void onKeyUp(KeyUpEvent event) {
1666         if (enableDebug) {
1667             debug("VFS: onKeyUp(" + event.getNativeKeyCode() + ")");
1668         }
1669         if (enabled && !readonly) {
1670             switch (event.getNativeKeyCode()) {
1671             case KeyCodes.KEY_ENTER:
1672             case KeyCodes.KEY_TAB:
1673             case KeyCodes.KEY_SHIFT:
1674             case KeyCodes.KEY_CTRL:
1675             case KeyCodes.KEY_ALT:
1676             case KeyCodes.KEY_DOWN:
1677             case KeyCodes.KEY_UP:
1678             case KeyCodes.KEY_PAGEDOWN:
1679             case KeyCodes.KEY_PAGEUP:
1680             case KeyCodes.KEY_ESCAPE:
1681                 ; // NOP
1682                 break;
1683             default:
1684                 if (textInputEnabled) {
1685                     filterOptions(currentPage);
1686                 }
1687                 break;
1688             }
1689         }
1690     }
1691 
1692     /**
1693      * Resets the Select to its initial state
1694      */
1695     private void reset() {
1696         debug("VFS: reset()");
1697         if (currentSuggestion != null) {
1698             String text = currentSuggestion.getReplacementString();
1699             setPromptingOff(text);
1700             selectedOptionKey = currentSuggestion.key;
1701         } else {
1702             if (focused) {
1703                 setPromptingOff("");
1704             } else {
1705                 setPromptingOn();
1706             }
1707             selectedOptionKey = null;
1708         }
1709         lastFilter = "";
1710         suggestionPopup.hide();
1711     }
1712 
1713     /**
1714      * Listener for popupopener
1715      */
1716 
1717     @Override
1718     public void onClick(ClickEvent event) {
1719         debug("VFS: onClick()");
1720         if (textInputEnabled
1721                 && event.getNativeEvent().getEventTarget().cast() == tb
1722                         .getElement()) {
1723             // Don't process clicks on the text field if text input is enabled
1724             return;
1725         }
1726         if (enabled && !readonly) {
1727             // ask suggestionPopup if it was just closed, we are using GWT
1728             // Popup's auto close feature
1729             if (!suggestionPopup.isJustClosed()) {
1730                 // If a focus event is not going to be sent, send the options
1731                 // request immediately; otherwise queue in the same burst as the
1732                 // focus event. Fixes #8321.
1733                 boolean immediate = focused
1734                         || !client.hasEventListeners(this, EventId.FOCUS);
1735                 filterOptions(-1, "", immediate);
1736                 popupOpenerClicked = true;
1737                 lastFilter = "";
1738             }
1739             DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
1740             focus();
1741             tb.selectAll();
1742         }
1743     }
1744 
1745     /**
1746      * Update minimum width for FilterSelect textarea based on input prompt and
1747      * suggestions.
1748      * <p>
1749      * For internal use only. May be removed or replaced in the future.
1750      */
1751     public void updateSuggestionPopupMinWidth() {
1752         // used only to calculate minimum width
1753         String captions = Util.escapeHTML(inputPrompt);
1754 
1755         for (FilterSelectSuggestion suggestion : currentSuggestions) {
1756             // Collect captions so we can calculate minimum width for
1757             // textarea
1758             if (captions.length() > 0) {
1759                 captions += "|";
1760             }
1761             captions += Util.escapeHTML(suggestion.getReplacementString());
1762         }
1763 
1764         // Calculate minimum textarea width
1765         suggestionPopupMinWidth = minWidth(captions);
1766     }
1767 
1768     /**
1769      * Calculate minimum width for FilterSelect textarea.
1770      * <p>
1771      * For internal use only. May be removed or replaced in the future.
1772      */
1773     public native int minWidth(String captions)
1774     /*-{
1775         if(!captions || captions.length <= 0)
1776                 return 0;
1777         captions = captions.split("|");
1778         var d = $wnd.document.createElement("div");
1779         var html = "";
1780         for(var i=0; i < captions.length; i++) {
1781                 html += "<div>" + captions[i] + "</div>";
1782                 // TODO apply same CSS classname as in suggestionmenu
1783         }
1784         d.style.position = "absolute";
1785         d.style.top = "0";
1786         d.style.left = "0";
1787         d.style.visibility = "hidden";
1788         d.innerHTML = html;
1789         $wnd.document.body.appendChild(d);
1790         var w = d.offsetWidth;
1791         $wnd.document.body.removeChild(d);
1792         return w;
1793     }-*/;
1794 
1795     /**
1796      * A flag which prevents a focus event from taking place
1797      */
1798     boolean iePreventNextFocus = false;
1799 
1800     /*
1801      * (non-Javadoc)
1802      * 
1803      * @see
1804      * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
1805      * .dom.client.FocusEvent)
1806      */
1807 
1808     @Override
1809     public void onFocus(FocusEvent event) {
1810         debug("VFS: onFocus()");
1811 
1812         /*
1813          * When we disable a blur event in ie we need to refocus the textfield.
1814          * This will cause a focus event we do not want to process, so in that
1815          * case we just ignore it.
1816          */
1817         if (BrowserInfo.get().isIE() && iePreventNextFocus) {
1818             iePreventNextFocus = false;
1819             return;
1820         }
1821 
1822         focused = true;
1823         if (prompting && !readonly) {
1824             setPromptingOff("");
1825         }
1826         addStyleDependentName("focus");
1827 
1828         if (client.hasEventListeners(this, EventId.FOCUS)) {
1829             client.updateVariable(paintableId, EventId.FOCUS, "", true);
1830         }
1831     }
1832 
1833     /**
1834      * A flag which cancels the blur event and sets the focus back to the
1835      * textfield if the Browser is IE
1836      */
1837     boolean preventNextBlurEventInIE = false;
1838 
1839     /*
1840      * (non-Javadoc)
1841      * 
1842      * @see
1843      * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
1844      * .dom.client.BlurEvent)
1845      */
1846 
1847     @Override
1848     public void onBlur(BlurEvent event) {
1849         debug("VFS: onBlur()");
1850 
1851         if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) {
1852             /*
1853              * Clicking in the suggestion popup or on the popup button in IE
1854              * causes a blur event to be sent for the field. In other browsers
1855              * this is prevented by canceling/preventing default behavior for
1856              * the focus event, in IE we handle it here by refocusing the text
1857              * field and ignoring the resulting focus event for the textfield
1858              * (in onFocus).
1859              */
1860             preventNextBlurEventInIE = false;
1861 
1862             Element focusedElement = Util.getIEFocusedElement();
1863             if (getElement().isOrHasChild(focusedElement)
1864                     || suggestionPopup.getElement()
1865                             .isOrHasChild(focusedElement)) {
1866 
1867                 // IF the suggestion popup or another part of the VFilterSelect
1868                 // was focused, move the focus back to the textfield and prevent
1869                 // the triggered focus event (in onFocus).
1870                 iePreventNextFocus = true;
1871                 tb.setFocus(true);
1872                 return;
1873             }
1874         }
1875 
1876         focused = false;
1877         if (!readonly) {
1878             // much of the TAB handling takes place here
1879             if (tabPressedWhenPopupOpen) {
1880                 tabPressedWhenPopupOpen = false;
1881                 suggestionPopup.menu.doSelectedItemAction();
1882                 suggestionPopup.hide();
1883             } else if ((!suggestionPopup.isAttached() && waitingForFilteringResponse)
1884                     || suggestionPopup.isJustClosed()) {
1885                 // typing so fast the popup was never opened, or it's just
1886                 // closed
1887                 waitingForFilteringResponse = false;
1888                 suggestionPopup.menu.doSelectedItemAction();
1889             }
1890             if (selectedOptionKey == null) {
1891                 setPromptingOn();
1892             } else if (currentSuggestion != null) {
1893                 setPromptingOff(currentSuggestion.caption);
1894             }
1895         }
1896         removeStyleDependentName("focus");
1897 
1898         if (client.hasEventListeners(this, EventId.BLUR)) {
1899             client.updateVariable(paintableId, EventId.BLUR, "", true);
1900         }
1901     }
1902 
1903     /*
1904      * (non-Javadoc)
1905      * 
1906      * @see com.vaadin.client.Focusable#focus()
1907      */
1908 
1909     @Override
1910     public void focus() {
1911         debug("VFS: focus()");
1912         focused = true;
1913         if (prompting && !readonly) {
1914             setPromptingOff("");
1915         }
1916         tb.setFocus(true);
1917     }
1918 
1919     /**
1920      * Calculates the width of the select if the select has undefined width.
1921      * Should be called when the width changes or when the icon changes.
1922      * <p>
1923      * For internal use only. May be removed or replaced in the future.
1924      */
1925     public void updateRootWidth() {
1926         ComponentConnector paintable = ConnectorMap.get(client).getConnector(
1927                 this);
1928         if (paintable.isUndefinedWidth()) {
1929 
1930             /*
1931              * When the select has a undefined with we need to check that we are
1932              * only setting the text box width relative to the first page width
1933              * of the items. If this is not done the text box width will change
1934              * when the popup is used to view longer items than the text box is
1935              * wide.
1936              */
1937             int w = Util.getRequiredWidth(this);
1938             if ((!initDone || currentPage + 1 < 0)
1939                     && suggestionPopupMinWidth > w) {
1940                 /*
1941                  * We want to compensate for the paddings just to preserve the
1942                  * exact size as in Vaadin 6.x, but we get here before
1943                  * MeasuredSize has been initialized.
1944                  * Util.measureHorizontalPaddingAndBorder does not work with
1945                  * border-box, so we must do this the hard way.
1946                  */
1947                 Style style = getElement().getStyle();
1948                 String originalPadding = style.getPadding();
1949                 String originalBorder = style.getBorderWidth();
1950                 style.setPaddingLeft(0, Unit.PX);
1951                 style.setBorderWidth(0, Unit.PX);
1952                 int offset = w - Util.getRequiredWidth(this);
1953                 style.setProperty("padding", originalPadding);
1954                 style.setProperty("borderWidth", originalBorder);
1955 
1956                 setWidth(suggestionPopupMinWidth + offset + "px");
1957             }
1958 
1959             /*
1960              * Lock the textbox width to its current value if it's not already
1961              * locked
1962              */
1963             if (!tb.getElement().getStyle().getWidth().endsWith("px")) {
1964                 tb.setWidth((tb.getOffsetWidth() - selectedItemIcon
1965                         .getOffsetWidth()) + "px");
1966             }
1967         }
1968     }
1969 
1970     /**
1971      * Get the width of the select in pixels where the text area and icon has
1972      * been included.
1973      * 
1974      * @return The width in pixels
1975      */
1976     private int getMainWidth() {
1977         return getOffsetWidth();
1978     }
1979 
1980     @Override
1981     public void setWidth(String width) {
1982         super.setWidth(width);
1983         if (width.length() != 0) {
1984             tb.setWidth("100%");
1985         }
1986     }
1987 
1988     /**
1989      * Handles special behavior of the mouse down event
1990      * 
1991      * @param event
1992      */
1993     private void handleMouseDownEvent(Event event) {
1994         /*
1995          * Prevent the keyboard focus from leaving the textfield by preventing
1996          * the default behaviour of the browser. Fixes #4285.
1997          */
1998         if (event.getTypeInt() == Event.ONMOUSEDOWN) {
1999             event.preventDefault();
2000             event.stopPropagation();
2001 
2002             /*
2003              * In IE the above wont work, the blur event will still trigger. So,
2004              * we set a flag here to prevent the next blur event from happening.
2005              * This is not needed if do not already have focus, in that case
2006              * there will not be any blur event and we should not cancel the
2007              * next blur.
2008              */
2009             if (BrowserInfo.get().isIE() && focused) {
2010                 preventNextBlurEventInIE = true;
2011                 debug("VFS: Going to prevent next blur event on IE");
2012             }
2013         }
2014     }
2015 
2016     @Override
2017     protected void onDetach() {
2018         super.onDetach();
2019         suggestionPopup.hide();
2020     }
2021 
2022     @Override
2023     public Element getSubPartElement(String subPart) {
2024         if ("textbox".equals(subPart)) {
2025             return tb.getElement();
2026         } else if ("button".equals(subPart)) {
2027             return popupOpener.getElement();
2028         }
2029         return null;
2030     }
2031 
2032     @Override
2033     public String getSubPartName(Element subElement) {
2034         if (tb.getElement().isOrHasChild(subElement)) {
2035             return "textbox";
2036         } else if (popupOpener.getElement().isOrHasChild(subElement)) {
2037             return "button";
2038         }
2039         return null;
2040     }
2041 
2042     @Override
2043     public void setAriaRequired(boolean required) {
2044         AriaHelper.handleInputRequired(tb, required);
2045     }
2046 
2047     @Override
2048     public void setAriaInvalid(boolean invalid) {
2049         AriaHelper.handleInputInvalid(tb, invalid);
2050     }
2051 
2052     @Override
2053     public void bindAriaCaption(Element captionElement) {
2054         AriaHelper.bindCaption(tb, captionElement);
2055     }
2056 }