You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComboBox.java 33KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. /*
  2. * Copyright 2000-2014 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. package com.vaadin.ui;
  17. import java.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.Iterator;
  21. import java.util.LinkedList;
  22. import java.util.List;
  23. import java.util.Map;
  24. import com.vaadin.data.Container;
  25. import com.vaadin.data.util.filter.SimpleStringFilter;
  26. import com.vaadin.event.FieldEvents;
  27. import com.vaadin.event.FieldEvents.BlurEvent;
  28. import com.vaadin.event.FieldEvents.BlurListener;
  29. import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
  30. import com.vaadin.event.FieldEvents.FocusEvent;
  31. import com.vaadin.event.FieldEvents.FocusListener;
  32. import com.vaadin.server.PaintException;
  33. import com.vaadin.server.PaintTarget;
  34. import com.vaadin.server.Resource;
  35. import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
  36. import com.vaadin.shared.ui.combobox.ComboBoxState;
  37. import com.vaadin.shared.ui.combobox.FilteringMode;
  38. /**
  39. * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's
  40. * turned of by default to avoid mistakes. Items are filtered based on user
  41. * input, and loaded dynamically ("lazy-loading") from the server. You can turn
  42. * on newItemsAllowed and change filtering mode (and also turn it off), but you
  43. * can not turn on multi-select mode.
  44. *
  45. */
  46. @SuppressWarnings("serial")
  47. public class ComboBox extends AbstractSelect implements
  48. AbstractSelect.Filtering, FieldEvents.BlurNotifier,
  49. FieldEvents.FocusNotifier {
  50. /**
  51. * ItemStyleGenerator can be used to add custom styles to combo box items
  52. * shown in the popup. The CSS class name that will be added to the item
  53. * style names is <tt>v-filterselect-item-[style name]</tt>.
  54. *
  55. * @since 7.5.6
  56. * @see ComboBox#setItemStyleGenerator(ItemStyleGenerator)
  57. */
  58. public interface ItemStyleGenerator extends Serializable {
  59. /**
  60. * Called by ComboBox when an item is painted.
  61. *
  62. * @param source
  63. * the source combo box
  64. * @param itemId
  65. * The itemId of the item to be painted. Can be
  66. * <code>null</code> if null selection is allowed.
  67. * @return The style name to add to this item. (the CSS class name will
  68. * be v-filterselect-item-[style name]
  69. */
  70. public String getStyle(ComboBox source, Object itemId);
  71. }
  72. private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
  73. @Override
  74. public void createNewItem(String itemValue) {
  75. if (isNewItemsAllowed()) {
  76. // New option entered (and it is allowed)
  77. if (itemValue != null && itemValue.length() > 0) {
  78. getNewItemHandler().addNewItem(itemValue);
  79. // rebuild list
  80. filterstring = null;
  81. prevfilterstring = null;
  82. }
  83. }
  84. }
  85. };
  86. FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) {
  87. @Override
  88. protected void fireEvent(LegacyEvent event) {
  89. ComboBox.this.fireEvent(event);
  90. }
  91. };
  92. /**
  93. * Holds value of property pageLength. 0 disables paging.
  94. */
  95. protected int pageLength = 10;
  96. // Current page when the user is 'paging' trough options
  97. private int currentPage = -1;
  98. private FilteringMode filteringMode = FilteringMode.STARTSWITH;
  99. private String filterstring;
  100. private String prevfilterstring;
  101. /**
  102. * Number of options that pass the filter, excluding the null item if any.
  103. */
  104. private int filteredSize;
  105. /**
  106. * Cache of filtered options, used only by the in-memory filtering system.
  107. */
  108. private List<Object> filteredOptions;
  109. /**
  110. * Flag to indicate that request repaint is called by filter request only
  111. */
  112. private boolean optionRequest;
  113. /**
  114. * True while painting to suppress item set change notifications that could
  115. * be caused by temporary filtering.
  116. */
  117. private boolean isPainting;
  118. /**
  119. * Flag to indicate whether to scroll the selected item visible (select the
  120. * page on which it is) when opening the popup or not. Only applies to
  121. * single select mode.
  122. *
  123. * This requires finding the index of the item, which can be expensive in
  124. * many large lazy loading containers.
  125. */
  126. private boolean scrollToSelectedItem = true;
  127. private String suggestionPopupWidth = null;
  128. /**
  129. * If text input is not allowed, the ComboBox behaves like a pretty
  130. * NativeSelect - the user can not enter any text and clicking the text
  131. * field opens the drop down with options
  132. */
  133. private boolean textInputAllowed = true;
  134. private ItemStyleGenerator itemStyleGenerator = null;
  135. public ComboBox() {
  136. init();
  137. }
  138. public ComboBox(String caption, Collection<?> options) {
  139. super(caption, options);
  140. init();
  141. }
  142. public ComboBox(String caption, Container dataSource) {
  143. super(caption, dataSource);
  144. init();
  145. }
  146. public ComboBox(String caption) {
  147. super(caption);
  148. init();
  149. }
  150. /**
  151. * Initialize the ComboBox with default settings and register client to
  152. * server RPC implementation.
  153. */
  154. private void init() {
  155. registerRpc(rpc);
  156. registerRpc(focusBlurRpc);
  157. setNewItemsAllowed(false);
  158. setImmediate(true);
  159. }
  160. /**
  161. * Gets the current input prompt.
  162. *
  163. * @see #setInputPrompt(String)
  164. * @return the current input prompt, or null if not enabled
  165. */
  166. public String getInputPrompt() {
  167. return getState(false).inputPrompt;
  168. }
  169. /**
  170. * Sets the input prompt - a textual prompt that is displayed when the
  171. * select would otherwise be empty, to prompt the user for input.
  172. *
  173. * @param inputPrompt
  174. * the desired input prompt, or null to disable
  175. */
  176. public void setInputPrompt(String inputPrompt) {
  177. getState().inputPrompt = inputPrompt;
  178. }
  179. private boolean isFilteringNeeded() {
  180. return filterstring != null && filterstring.length() > 0
  181. && filteringMode != FilteringMode.OFF;
  182. }
  183. @Override
  184. public void paintContent(PaintTarget target) throws PaintException {
  185. isPainting = true;
  186. try {
  187. // clear caption change listeners
  188. getCaptionChangeListener().clear();
  189. // The tab ordering number
  190. if (getTabIndex() != 0) {
  191. target.addAttribute("tabindex", getTabIndex());
  192. }
  193. // If the field is modified, but not committed, set modified
  194. // attribute
  195. if (isModified()) {
  196. target.addAttribute("modified", true);
  197. }
  198. if (isNewItemsAllowed()) {
  199. target.addAttribute("allownewitem", true);
  200. }
  201. boolean needNullSelectOption = false;
  202. if (isNullSelectionAllowed()) {
  203. target.addAttribute("nullselect", true);
  204. needNullSelectOption = (getNullSelectionItemId() == null);
  205. if (!needNullSelectOption) {
  206. target.addAttribute("nullselectitem", true);
  207. }
  208. }
  209. // Constructs selected keys array
  210. String[] selectedKeys = new String[(getValue() == null
  211. && getNullSelectionItemId() == null ? 0 : 1)];
  212. target.addAttribute("pagelength", pageLength);
  213. if (suggestionPopupWidth != null) {
  214. target.addAttribute("suggestionPopupWidth",
  215. suggestionPopupWidth);
  216. }
  217. target.addAttribute("filteringmode", getFilteringMode().toString());
  218. // Paints the options and create array of selected id keys
  219. int keyIndex = 0;
  220. target.startTag("options");
  221. if (currentPage < 0) {
  222. optionRequest = false;
  223. currentPage = 0;
  224. filterstring = "";
  225. }
  226. boolean nullFilteredOut = isFilteringNeeded();
  227. // null option is needed and not filtered out, even if not on
  228. // current
  229. // page
  230. boolean nullOptionVisible = needNullSelectOption
  231. && !nullFilteredOut;
  232. // first try if using container filters is possible
  233. List<?> options = getOptionsWithFilter(nullOptionVisible);
  234. if (null == options) {
  235. // not able to use container filters, perform explicit in-memory
  236. // filtering
  237. options = getFilteredOptions();
  238. filteredSize = options.size();
  239. options = sanitetizeList(options, nullOptionVisible);
  240. }
  241. final boolean paintNullSelection = needNullSelectOption
  242. && currentPage == 0 && !nullFilteredOut;
  243. if (paintNullSelection) {
  244. target.startTag("so");
  245. target.addAttribute("caption", "");
  246. target.addAttribute("key", "");
  247. paintItemStyle(target, null);
  248. target.endTag("so");
  249. }
  250. final Iterator<?> i = options.iterator();
  251. // Paints the available selection options from data source
  252. while (i.hasNext()) {
  253. final Object id = i.next();
  254. if (!isNullSelectionAllowed() && id != null
  255. && id.equals(getNullSelectionItemId())
  256. && !isSelected(id)) {
  257. continue;
  258. }
  259. // Gets the option attribute values
  260. final String key = itemIdMapper.key(id);
  261. final String caption = getItemCaption(id);
  262. final Resource icon = getItemIcon(id);
  263. getCaptionChangeListener().addNotifierForItem(id);
  264. // Paints the option
  265. target.startTag("so");
  266. if (icon != null) {
  267. target.addAttribute("icon", icon);
  268. }
  269. target.addAttribute("caption", caption);
  270. if (id != null && id.equals(getNullSelectionItemId())) {
  271. target.addAttribute("nullselection", true);
  272. }
  273. target.addAttribute("key", key);
  274. if (keyIndex < selectedKeys.length && isSelected(id)) {
  275. // at most one item can be selected at a time
  276. selectedKeys[keyIndex++] = key;
  277. }
  278. paintItemStyle(target, id);
  279. target.endTag("so");
  280. }
  281. target.endTag("options");
  282. target.addAttribute("totalitems", size()
  283. + (needNullSelectOption ? 1 : 0));
  284. if (filteredSize > 0 || nullOptionVisible) {
  285. target.addAttribute("totalMatches", filteredSize
  286. + (nullOptionVisible ? 1 : 0));
  287. }
  288. // Paint variables
  289. target.addVariable(this, "selected", selectedKeys);
  290. if (getValue() != null && selectedKeys[0] == null) {
  291. // not always available, e.g. scrollToSelectedIndex=false
  292. // Give the caption for selected item still, not to make it look
  293. // like there is no selection at all
  294. target.addAttribute("selectedCaption",
  295. getItemCaption(getValue()));
  296. }
  297. if (isNewItemsAllowed()) {
  298. target.addVariable(this, "newitem", "");
  299. }
  300. target.addVariable(this, "filter", filterstring);
  301. target.addVariable(this, "page", currentPage);
  302. currentPage = -1; // current page is always set by client
  303. optionRequest = true;
  304. } finally {
  305. isPainting = false;
  306. }
  307. }
  308. private void paintItemStyle(PaintTarget target, Object itemId)
  309. throws PaintException {
  310. if (itemStyleGenerator != null) {
  311. String style = itemStyleGenerator.getStyle(this, itemId);
  312. if (style != null && !style.isEmpty()) {
  313. target.addAttribute("style", style);
  314. }
  315. }
  316. }
  317. /**
  318. * Sets whether it is possible to input text into the field or whether the
  319. * field area of the component is just used to show what is selected. By
  320. * disabling text input, the comboBox will work in the same way as a
  321. * {@link NativeSelect}
  322. *
  323. * @see #isTextInputAllowed()
  324. *
  325. * @param textInputAllowed
  326. * true to allow entering text, false to just show the current
  327. * selection
  328. */
  329. public void setTextInputAllowed(boolean textInputAllowed) {
  330. getState().textInputAllowed = textInputAllowed;
  331. }
  332. /**
  333. * Returns true if the user can enter text into the field to either filter
  334. * the selections or enter a new value if {@link #isNewItemsAllowed()}
  335. * returns true. If text input is disabled, the comboBox will work in the
  336. * same way as a {@link NativeSelect}
  337. *
  338. * @return
  339. */
  340. public boolean isTextInputAllowed() {
  341. return getState(false).textInputAllowed;
  342. }
  343. @Override
  344. protected ComboBoxState getState() {
  345. return (ComboBoxState) super.getState();
  346. }
  347. @Override
  348. protected ComboBoxState getState(boolean markAsDirty) {
  349. return (ComboBoxState) super.getState(markAsDirty);
  350. }
  351. /**
  352. * Returns the filtered options for the current page using a container
  353. * filter.
  354. *
  355. * As a size effect, {@link #filteredSize} is set to the total number of
  356. * items passing the filter.
  357. *
  358. * The current container must be {@link Filterable} and {@link Indexed}, and
  359. * the filtering mode must be suitable for container filtering (tested with
  360. * {@link #canUseContainerFilter()}).
  361. *
  362. * Use {@link #getFilteredOptions()} and
  363. * {@link #sanitetizeList(List, boolean)} if this is not the case.
  364. *
  365. * @param needNullSelectOption
  366. * @return filtered list of options (may be empty) or null if cannot use
  367. * container filters
  368. */
  369. protected List<?> getOptionsWithFilter(boolean needNullSelectOption) {
  370. Container container = getContainerDataSource();
  371. if (pageLength == 0 && !isFilteringNeeded()) {
  372. // no paging or filtering: return all items
  373. filteredSize = container.size();
  374. assert filteredSize >= 0;
  375. return new ArrayList<Object>(container.getItemIds());
  376. }
  377. if (!(container instanceof Filterable)
  378. || !(container instanceof Indexed)
  379. || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) {
  380. return null;
  381. }
  382. Filterable filterable = (Filterable) container;
  383. Filter filter = buildFilter(filterstring, filteringMode);
  384. // adding and removing filters leads to extraneous item set
  385. // change events from the underlying container, but the ComboBox does
  386. // not process or propagate them based on the flag filteringContainer
  387. if (filter != null) {
  388. filterable.addContainerFilter(filter);
  389. }
  390. // try-finally to ensure that the filter is removed from container even
  391. // if a exception is thrown...
  392. try {
  393. Indexed indexed = (Indexed) container;
  394. int indexToEnsureInView = -1;
  395. // if not an option request (item list when user changes page), go
  396. // to page with the selected item after filtering if accepted by
  397. // filter
  398. Object selection = getValue();
  399. if (isScrollToSelectedItem() && !optionRequest && selection != null) {
  400. // ensure proper page
  401. indexToEnsureInView = indexed.indexOfId(selection);
  402. }
  403. filteredSize = container.size();
  404. assert filteredSize >= 0;
  405. currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
  406. indexToEnsureInView, filteredSize);
  407. int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
  408. filteredSize);
  409. int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
  410. filteredSize, first);
  411. // Compute the number of items to fetch from the indexes given or
  412. // based on the filtered size of the container
  413. int lastItemToFetch = Math.min(last, filteredSize - 1);
  414. int nrOfItemsToFetch = (lastItemToFetch + 1) - first;
  415. List<?> options = indexed.getItemIds(first, nrOfItemsToFetch);
  416. return options;
  417. } finally {
  418. // to the outside, filtering should not be visible
  419. if (filter != null) {
  420. filterable.removeContainerFilter(filter);
  421. }
  422. }
  423. }
  424. /**
  425. * Constructs a filter instance to use when using a Filterable container in
  426. * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode.
  427. *
  428. * Note that the client side implementation expects the filter string to
  429. * apply to the item caption string it sees, so changing the behavior of
  430. * this method can cause problems.
  431. *
  432. * @param filterString
  433. * @param filteringMode
  434. * @return
  435. */
  436. protected Filter buildFilter(String filterString,
  437. FilteringMode filteringMode) {
  438. Filter filter = null;
  439. if (null != filterString && !"".equals(filterString)) {
  440. switch (filteringMode) {
  441. case OFF:
  442. break;
  443. case STARTSWITH:
  444. filter = new SimpleStringFilter(getItemCaptionPropertyId(),
  445. filterString, true, true);
  446. break;
  447. case CONTAINS:
  448. filter = new SimpleStringFilter(getItemCaptionPropertyId(),
  449. filterString, true, false);
  450. break;
  451. }
  452. }
  453. return filter;
  454. }
  455. @Override
  456. public void containerItemSetChange(Container.ItemSetChangeEvent event) {
  457. if (!isPainting) {
  458. super.containerItemSetChange(event);
  459. }
  460. }
  461. /**
  462. * Makes correct sublist of given list of options.
  463. *
  464. * If paint is not an option request (affected by page or filter change),
  465. * page will be the one where possible selection exists.
  466. *
  467. * Detects proper first and last item in list to return right page of
  468. * options. Also, if the current page is beyond the end of the list, it will
  469. * be adjusted.
  470. *
  471. * @param options
  472. * @param needNullSelectOption
  473. * flag to indicate if nullselect option needs to be taken into
  474. * consideration
  475. */
  476. private List<?> sanitetizeList(List<?> options, boolean needNullSelectOption) {
  477. if (pageLength != 0 && options.size() > pageLength) {
  478. int indexToEnsureInView = -1;
  479. // if not an option request (item list when user changes page), go
  480. // to page with the selected item after filtering if accepted by
  481. // filter
  482. Object selection = getValue();
  483. if (isScrollToSelectedItem() && !optionRequest && selection != null) {
  484. // ensure proper page
  485. indexToEnsureInView = options.indexOf(selection);
  486. }
  487. int size = options.size();
  488. currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
  489. indexToEnsureInView, size);
  490. int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
  491. size);
  492. int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
  493. size, first);
  494. return options.subList(first, last + 1);
  495. } else {
  496. return options;
  497. }
  498. }
  499. /**
  500. * Returns the index of the first item on the current page. The index is to
  501. * the underlying (possibly filtered) contents. The null item, if any, does
  502. * not have an index but takes up a slot on the first page.
  503. *
  504. * @param needNullSelectOption
  505. * true if a null option should be shown before any other options
  506. * (takes up the first slot on the first page, not counted in
  507. * index)
  508. * @param size
  509. * number of items after filtering (not including the null item,
  510. * if any)
  511. * @return first item to show on the UI (index to the filtered list of
  512. * options, not taking the null item into consideration if any)
  513. */
  514. private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption,
  515. int size) {
  516. // Not all options are visible, find out which ones are on the
  517. // current "page".
  518. int first = currentPage * pageLength;
  519. if (needNullSelectOption && currentPage > 0) {
  520. first--;
  521. }
  522. return first;
  523. }
  524. /**
  525. * Returns the index of the last item on the current page. The index is to
  526. * the underlying (possibly filtered) contents. If needNullSelectOption is
  527. * true, the null item takes up the first slot on the first page,
  528. * effectively reducing the first page size by one.
  529. *
  530. * @param needNullSelectOption
  531. * true if a null option should be shown before any other options
  532. * (takes up the first slot on the first page, not counted in
  533. * index)
  534. * @param size
  535. * number of items after filtering (not including the null item,
  536. * if any)
  537. * @param first
  538. * index in the filtered view of the first item of the page
  539. * @return index in the filtered view of the last item on the page
  540. */
  541. private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption,
  542. int size, int first) {
  543. // page length usable for non-null items
  544. int effectivePageLength = pageLength
  545. - (needNullSelectOption && (currentPage == 0) ? 1 : 0);
  546. return Math.min(size - 1, first + effectivePageLength - 1);
  547. }
  548. /**
  549. * Adjusts the index of the current page if necessary: make sure the current
  550. * page is not after the end of the contents, and optionally go to the page
  551. * containg a specific item. There are no side effects but the adjusted page
  552. * index is returned.
  553. *
  554. * @param page
  555. * page number to use as the starting point
  556. * @param needNullSelectOption
  557. * true if a null option should be shown before any other options
  558. * (takes up the first slot on the first page, not counted in
  559. * index)
  560. * @param indexToEnsureInView
  561. * index of an item that should be included on the page (in the
  562. * data set, not counting the null item if any), -1 for none
  563. * @param size
  564. * number of items after filtering (not including the null item,
  565. * if any)
  566. */
  567. private int adjustCurrentPage(int page, boolean needNullSelectOption,
  568. int indexToEnsureInView, int size) {
  569. if (indexToEnsureInView != -1) {
  570. int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0))
  571. / pageLength;
  572. page = newPage;
  573. }
  574. // adjust the current page if beyond the end of the list
  575. if (page * pageLength > size) {
  576. page = (size + (needNullSelectOption ? 1 : 0)) / pageLength;
  577. }
  578. return page;
  579. }
  580. /**
  581. * Filters the options in memory and returns the full filtered list.
  582. *
  583. * This can be less efficient than using container filters, so use
  584. * {@link #getOptionsWithFilter(boolean)} if possible (filterable container
  585. * and suitable item caption mode etc.).
  586. *
  587. * @return
  588. */
  589. protected List<?> getFilteredOptions() {
  590. if (!isFilteringNeeded()) {
  591. prevfilterstring = null;
  592. filteredOptions = new LinkedList<Object>(getItemIds());
  593. return filteredOptions;
  594. }
  595. if (filterstring.equals(prevfilterstring)) {
  596. return filteredOptions;
  597. }
  598. Collection<?> items;
  599. if (prevfilterstring != null
  600. && filterstring.startsWith(prevfilterstring)) {
  601. items = filteredOptions;
  602. } else {
  603. items = getItemIds();
  604. }
  605. prevfilterstring = filterstring;
  606. filteredOptions = new LinkedList<Object>();
  607. for (final Iterator<?> it = items.iterator(); it.hasNext();) {
  608. final Object itemId = it.next();
  609. String caption = getItemCaption(itemId);
  610. if (caption == null || caption.equals("")) {
  611. continue;
  612. } else {
  613. caption = caption.toLowerCase(getLocale());
  614. }
  615. switch (filteringMode) {
  616. case CONTAINS:
  617. if (caption.indexOf(filterstring) > -1) {
  618. filteredOptions.add(itemId);
  619. }
  620. break;
  621. case STARTSWITH:
  622. default:
  623. if (caption.startsWith(filterstring)) {
  624. filteredOptions.add(itemId);
  625. }
  626. break;
  627. }
  628. }
  629. return filteredOptions;
  630. }
  631. /**
  632. * Invoked when the value of a variable has changed.
  633. *
  634. * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
  635. * java.util.Map)
  636. */
  637. @Override
  638. public void changeVariables(Object source, Map<String, Object> variables) {
  639. // Not calling super.changeVariables due the history of select
  640. // component hierarchy
  641. // Selection change
  642. if (variables.containsKey("selected")) {
  643. final String[] ka = (String[]) variables.get("selected");
  644. // Single select mode
  645. if (ka.length == 0) {
  646. // Allows deselection only if the deselected item is visible
  647. final Object current = getValue();
  648. final Collection<?> visible = getVisibleItemIds();
  649. if (visible != null && visible.contains(current)) {
  650. setValue(null, true);
  651. }
  652. } else {
  653. final Object id = itemIdMapper.get(ka[0]);
  654. if (id != null && id.equals(getNullSelectionItemId())) {
  655. setValue(null, true);
  656. } else {
  657. setValue(id, true);
  658. }
  659. }
  660. }
  661. String newFilter;
  662. if ((newFilter = (String) variables.get("filter")) != null) {
  663. // this is a filter request
  664. currentPage = ((Integer) variables.get("page")).intValue();
  665. filterstring = newFilter;
  666. if (filterstring != null) {
  667. filterstring = filterstring.toLowerCase(getLocale());
  668. }
  669. requestRepaint();
  670. }
  671. }
  672. @Override
  673. public void setFilteringMode(FilteringMode filteringMode) {
  674. this.filteringMode = filteringMode;
  675. }
  676. @Override
  677. public FilteringMode getFilteringMode() {
  678. return filteringMode;
  679. }
  680. @Override
  681. public void addBlurListener(BlurListener listener) {
  682. addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  683. BlurListener.blurMethod);
  684. }
  685. /**
  686. * @deprecated As of 7.0, replaced by {@link #addBlurListener(BlurListener)}
  687. **/
  688. @Override
  689. @Deprecated
  690. public void addListener(BlurListener listener) {
  691. addBlurListener(listener);
  692. }
  693. @Override
  694. public void removeBlurListener(BlurListener listener) {
  695. removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
  696. }
  697. /**
  698. * @deprecated As of 7.0, replaced by
  699. * {@link #removeBlurListener(BlurListener)}
  700. **/
  701. @Override
  702. @Deprecated
  703. public void removeListener(BlurListener listener) {
  704. removeBlurListener(listener);
  705. }
  706. @Override
  707. public void addFocusListener(FocusListener listener) {
  708. addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  709. FocusListener.focusMethod);
  710. }
  711. /**
  712. * @deprecated As of 7.0, replaced by
  713. * {@link #addFocusListener(FocusListener)}
  714. **/
  715. @Override
  716. @Deprecated
  717. public void addListener(FocusListener listener) {
  718. addFocusListener(listener);
  719. }
  720. @Override
  721. public void removeFocusListener(FocusListener listener) {
  722. removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
  723. }
  724. /**
  725. * @deprecated As of 7.0, replaced by
  726. * {@link #removeFocusListener(FocusListener)}
  727. **/
  728. @Override
  729. @Deprecated
  730. public void removeListener(FocusListener listener) {
  731. removeFocusListener(listener);
  732. }
  733. /**
  734. * ComboBox does not support multi select mode.
  735. *
  736. * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
  737. * {@link TwinColSelect} instead
  738. * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean)
  739. * @throws UnsupportedOperationException
  740. * if trying to activate multiselect mode
  741. */
  742. @Deprecated
  743. @Override
  744. public void setMultiSelect(boolean multiSelect) {
  745. if (multiSelect) {
  746. throw new UnsupportedOperationException("Multiselect not supported");
  747. }
  748. }
  749. /**
  750. * ComboBox does not support multi select mode.
  751. *
  752. * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
  753. * {@link TwinColSelect} instead
  754. *
  755. * @see com.vaadin.ui.AbstractSelect#isMultiSelect()
  756. *
  757. * @return false
  758. */
  759. @Deprecated
  760. @Override
  761. public boolean isMultiSelect() {
  762. return false;
  763. }
  764. /**
  765. * Returns the page length of the suggestion popup.
  766. *
  767. * @return the pageLength
  768. */
  769. public int getPageLength() {
  770. return pageLength;
  771. }
  772. /**
  773. * Returns the suggestion pop-up's width as a CSS string.
  774. *
  775. * @see #setPopupWidth
  776. * @since 7.7
  777. */
  778. public String getPopupWidth() {
  779. return suggestionPopupWidth;
  780. }
  781. /**
  782. * Sets the page length for the suggestion popup. Setting the page length to
  783. * 0 will disable suggestion popup paging (all items visible).
  784. *
  785. * @param pageLength
  786. * the pageLength to set
  787. */
  788. public void setPageLength(int pageLength) {
  789. this.pageLength = pageLength;
  790. markAsDirty();
  791. }
  792. /**
  793. * Sets the suggestion pop-up's width as a CSS string. By using relative
  794. * units (e.g. "50%") it's possible to set the popup's width relative to the
  795. * ComboBox itself.
  796. *
  797. * @see #getPopupWidth()
  798. * @since 7.7
  799. * @param width the width
  800. */
  801. public void setPopupWidth(String width) {
  802. suggestionPopupWidth = width;
  803. markAsDirty();
  804. }
  805. /**
  806. * Sets whether to scroll the selected item visible (directly open the page
  807. * on which it is) when opening the combo box popup or not. Only applies to
  808. * single select mode.
  809. *
  810. * This requires finding the index of the item, which can be expensive in
  811. * many large lazy loading containers.
  812. *
  813. * @param scrollToSelectedItem
  814. * true to find the page with the selected item when opening the
  815. * selection popup
  816. */
  817. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  818. this.scrollToSelectedItem = scrollToSelectedItem;
  819. }
  820. /**
  821. * Returns true if the select should find the page with the selected item
  822. * when opening the popup (single select combo box only).
  823. *
  824. * @see #setScrollToSelectedItem(boolean)
  825. *
  826. * @return true if the page with the selected item will be shown when
  827. * opening the popup
  828. */
  829. public boolean isScrollToSelectedItem() {
  830. return scrollToSelectedItem;
  831. }
  832. /**
  833. * Sets the item style generator that is used to produce custom styles for
  834. * showing items in the popup. The CSS class name that will be added to the
  835. * item style names is <tt>v-filterselect-item-[style name]</tt>.
  836. *
  837. * @param itemStyleGenerator
  838. * the item style generator to set, or <code>null</code> to not
  839. * use any custom item styles
  840. * @since 7.5.6
  841. */
  842. public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
  843. this.itemStyleGenerator = itemStyleGenerator;
  844. markAsDirty();
  845. }
  846. /**
  847. * Gets the currently used item style generator.
  848. *
  849. * @return the itemStyleGenerator the currently used item style generator,
  850. * or <code>null</code> if no generator is used
  851. * @since 7.5.6
  852. */
  853. public ItemStyleGenerator getItemStyleGenerator() {
  854. return itemStyleGenerator;
  855. }
  856. }