1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """Defines a class representing a selection of items the user has selected
17 in a UI."""
18
19 from warnings import warn
20
21 from muntjac.event import field_events
22 from muntjac.data.util.filter.simple_string_filter import SimpleStringFilter
23 from muntjac.data.container import IContainer, IFilterable, IIndexed
24 from muntjac.ui import abstract_select
25
26 from muntjac.event.field_events import \
27 FocusEvent, BlurEvent, IBlurListener, IFocusListener
28
29
30 -class Select(abstract_select.AbstractSelect, abstract_select.IFiltering,
31 field_events.IBlurNotifier, field_events.IFocusNotifier):
32 """A class representing a selection of items the user has selected in a
33 UI. The set of choices is presented as a set of L{IItem}s in a
34 L{IContainer}.
35
36 A C{Select} component may be in single- or multiselect mode.
37 Multiselect mode means that more than one item can be selected
38 simultaneously.
39
40 @author: Vaadin Ltd.
41 @author: Richard Lincoln
42 @version: 1.1.2
43 """
44
45 CLIENT_WIDGET = None
46
48
49 self.pageLength = 10
50
51 self._columns = 0
52
53
54 self._currentPage = -1
55
56 self._filteringMode = self.FILTERINGMODE_STARTSWITH
57
58 self._filterstring = None
59 self._prevfilterstring = None
60
61
62
63 self._filteredSize = None
64
65
66
67 self._filteredOptions = None
68
69
70
71 self._optionRequest = None
72
73
74
75 self._filteringContainer = None
76
77
78
79
80
81
82
83 self._scrollToSelectedItem = True
84
85 nargs = len(args)
86 if nargs == 0:
87 super(Select, self).__init__()
88 elif nargs == 1:
89 caption, = args
90 super(Select, self).__init__(caption)
91 elif nargs == 2:
92 if isinstance(args[1], IContainer):
93 caption, dataSource = args
94 super(Select, self).__init__(caption, dataSource)
95 else:
96 caption, options = args
97 super(Select, self).__init__(caption, options)
98 else:
99 raise ValueError, 'too many arguments'
100
101
102 - def paintContent(self, target):
103 """Paints the content of this component.
104
105 @param target:
106 the Paint Event.
107 @raise PaintException:
108 if the paint operation failed.
109 """
110 if self.isMultiSelect():
111
112
113
114 super(Select, self).paintContent(target)
115
116 target.addAttribute('type', 'legacy-multi')
117 return
118
119
120 self.getCaptionChangeListener().clear()
121
122
123 if self.getTabIndex() != 0:
124 target.addAttribute('tabindex', self.getTabIndex())
125
126
127 if self.isModified():
128 target.addAttribute('modified', True)
129
130
131 if self.isRequired():
132 target.addAttribute('required', True)
133
134 if self.isNewItemsAllowed():
135 target.addAttribute('allownewitem', True)
136
137 needNullSelectOption = False
138 if self.isNullSelectionAllowed():
139 target.addAttribute('nullselect', True)
140 needNullSelectOption = self.getNullSelectionItemId() is None
141 if not needNullSelectOption:
142 target.addAttribute('nullselectitem', True)
143
144
145 if self.isMultiSelect():
146 selectedKeys = [None] * len(self.getValue())
147 elif self.getValue() is None and self.getNullSelectionItemId() is None:
148 selectedKeys = []
149 else:
150 selectedKeys = [None]
151
152 target.addAttribute('pagelength', self.pageLength)
153
154 target.addAttribute('filteringmode', self.getFilteringMode())
155
156
157 keyIndex = 0
158
159 target.startTag('options')
160
161 if self._currentPage < 0:
162 self._optionRequest = False
163 self._currentPage = 0
164 self._filterstring = ''
165
166 nullFilteredOut = ((self._filterstring is not None)
167 and (self._filterstring != '')
168 and (self._filteringMode != self.FILTERINGMODE_OFF))
169
170
171 nullOptionVisible = needNullSelectOption and not nullFilteredOut
172
173
174 options = self.getOptionsWithFilter(nullOptionVisible)
175 if None is options:
176
177
178 options = self.getFilteredOptions()
179 self._filteredSize = len(options)
180 options = self.sanitetizeList(options, nullOptionVisible)
181
182 paintNullSelection = (needNullSelectOption and (self._currentPage == 0)
183 and (not nullFilteredOut))
184
185 if paintNullSelection:
186 target.startTag('so')
187 target.addAttribute('caption', '')
188 target.addAttribute('key', '')
189 target.endTag('so')
190
191 i = iter(options)
192
193 while True:
194 try:
195 idd = i.next()
196
197 if ((not self.isNullSelectionAllowed())
198 and (idd is not None)
199 and (idd == self.getNullSelectionItemId())
200 and (not self.isSelected(idd))):
201 continue
202
203
204 key = self.itemIdMapper.key(idd)
205 caption = self.getItemCaption(idd)
206 icon = self.getItemIcon(idd)
207 self.getCaptionChangeListener().addNotifierForItem(idd)
208
209
210 target.startTag('so')
211 if icon is not None:
212 target.addAttribute('icon', icon)
213
214 target.addAttribute('caption', caption)
215 if (idd is not None) and idd == self.getNullSelectionItemId():
216 target.addAttribute('nullselection', True)
217
218 target.addAttribute('key', key)
219 if self.isSelected(idd) and (keyIndex < len(selectedKeys)):
220 target.addAttribute('selected', True)
221 selectedKeys[keyIndex] = key
222 keyIndex += 1
223
224 target.endTag('so')
225 except StopIteration:
226 break
227 target.endTag('options')
228
229 target.addAttribute('totalitems', (len(self)
230 + (1 if needNullSelectOption else 0)))
231
232 if (self._filteredSize > 0) or nullOptionVisible:
233 target.addAttribute('totalMatches', (self._filteredSize
234 + (1 if nullOptionVisible else 0)))
235
236
237 target.addVariable(self, 'selected', selectedKeys)
238 if self.isNewItemsAllowed():
239 target.addVariable(self, 'newitem', '')
240
241 target.addVariable(self, 'filter', self._filterstring)
242 target.addVariable(self, 'page', self._currentPage)
243
244 self._currentPage = -1
245
246 self._optionRequest = True
247
248
249 if self.shouldHideErrors():
250 target.addAttribute('hideErrors', True)
251
252
254 """Returns the filtered options for the current page using a container
255 filter.
256
257 As a size effect, L{filteredSize} is set to the total number of
258 items passing the filter.
259
260 The current container must be L{IFilterable} and L{IIndexed},
261 and the filtering mode must be suitable for container filtering
262 (tested with L{canUseContainerFilter}).
263
264 Use L{getFilteredOptions} and L{sanitetizeList} if this is not the
265 case.
266
267 @param needNullSelectOption:
268 @return: filtered list of options (may be empty) or null if cannot use
269 container filters
270 """
271 container = self.getContainerDataSource()
272
273 if self.pageLength == 0:
274
275 self._filteredSize = len(container)
276 return list(container.getItemIds())
277
278 if ((not isinstance(container, IFilterable))
279 or (not isinstance(container, IIndexed))
280 or (self.getItemCaptionMode() !=
281 self.ITEM_CAPTION_MODE_PROPERTY)):
282 return None
283
284 filterable = container
285
286 fltr = self.buildFilter(self._filterstring, self._filteringMode)
287
288
289
290
291 if fltr is not None:
292 self._filteringContainer = True
293 filterable.addContainerFilter(fltr)
294
295 indexed = container
296
297 indexToEnsureInView = -1
298
299
300
301
302 selection = self.getValue()
303 if (self.isScrollToSelectedItem() and (not self._optionRequest)
304 and (not self.isMultiSelect()) and (selection is not None)):
305
306 indexToEnsureInView = indexed.indexOfId(selection)
307
308 self._filteredSize = len(container)
309 self._currentPage = self.adjustCurrentPage(self._currentPage,
310 needNullSelectOption, indexToEnsureInView, self._filteredSize)
311 first = self.getFirstItemIndexOnCurrentPage(needNullSelectOption,
312 self._filteredSize)
313 last = self.getLastItemIndexOnCurrentPage(needNullSelectOption,
314 self._filteredSize, first)
315
316 options = list()
317 i = first
318 while (i <= last) and (i < self._filteredSize):
319 options.append( indexed.getIdByIndex(i) )
320 i += 1
321
322
323 if fltr is not None:
324 filterable.removeContainerFilter(fltr)
325 self._filteringContainer = False
326
327 return options
328
329
331 """Constructs a filter instance to use when using a IFilterable
332 container in the C{ITEM_CAPTION_MODE_PROPERTY} mode.
333
334 Note that the client side implementation expects the filter string to
335 apply to the item caption string it sees, so changing the behavior of
336 this method can cause problems.
337 """
338 fltr = None
339 if filterString is not None and filterString != '':
340 test = filteringMode
341 if test == self.FILTERINGMODE_OFF:
342 pass
343 elif test == self.FILTERINGMODE_STARTSWITH:
344 fltr = SimpleStringFilter(self.getItemCaptionPropertyId(),
345 filterString, True, True)
346 elif test == self.FILTERINGMODE_CONTAINS:
347 fltr = SimpleStringFilter(self.getItemCaptionPropertyId(),
348 filterString, True, False)
349 return fltr
350
351
355
356
358 """Makes correct sublist of given list of options.
359
360 If paint is not an option request (affected by page or filter change),
361 page will be the one where possible selection exists.
362
363 Detects proper first and last item in list to return right page of
364 options. Also, if the current page is beyond the end of the list, it
365 will be adjusted.
366
367 @param options:
368 @param needNullSelectOption:
369 flag to indicate if nullselect option needs to be taken
370 into consideration
371 """
372 if (self.pageLength != 0) and (len(options) > self.pageLength):
373
374 indexToEnsureInView = -1
375
376
377
378
379 selection = self.getValue()
380 if (self.isScrollToSelectedItem() and (not self._optionRequest)
381 and (not self.isMultiSelect())
382 and (selection is not None)):
383
384 try:
385 indexToEnsureInView = options.index(selection)
386 except ValueError:
387 indexToEnsureInView = -1
388
389 size = len(options)
390 self._currentPage = self.adjustCurrentPage(self._currentPage,
391 needNullSelectOption, indexToEnsureInView, size)
392 first = self.getFirstItemIndexOnCurrentPage(needNullSelectOption,
393 size)
394 last = self.getLastItemIndexOnCurrentPage(needNullSelectOption,
395 size, first)
396 return options[first:last + 1]
397 else:
398 return options
399
400
401 - def getFirstItemIndexOnCurrentPage(self, needNullSelectOption, size):
402 """Returns the index of the first item on the current page. The index
403 is to the underlying (possibly filtered) contents. The null item, if
404 any, does not have an index but takes up a slot on the first page.
405
406 @param needNullSelectOption:
407 true if a null option should be shown before any other
408 options (takes up the first slot on the first page, not
409 counted in index)
410 @param size:
411 number of items after filtering (not including the null
412 item, if any)
413 @return: first item to show on the UI (index to the filtered list of
414 options, not taking the null item into consideration if any)
415 """
416
417
418 first = self._currentPage * self.pageLength
419 if needNullSelectOption and (self._currentPage > 0):
420 first -= 1
421 return first
422
423
424 - def getLastItemIndexOnCurrentPage(self, needNullSelectOption, size, first):
425 """Returns the index of the last item on the current page. The index
426 is to the underlying (possibly filtered) contents. If
427 needNullSelectOption is true, the null item takes up the first slot
428 on the first page, effectively reducing the first page size by one.
429
430 @param needNullSelectOption:
431 true if a null option should be shown before any other
432 options (takes up the first slot on the first page, not
433 counted in index)
434 @param size:
435 number of items after filtering (not including the null
436 item, if any)
437 @param first:
438 index in the filtered view of the first item of the page
439 @return: index in the filtered view of the last item on the page
440 """
441
442 if needNullSelectOption and (self._currentPage == 0):
443 effectivePageLength = self.pageLength - 1
444 else:
445 effectivePageLength = self.pageLength
446
447 return min(size - 1, (first + effectivePageLength) - 1)
448
449
450 - def adjustCurrentPage(self, page, needNullSelectOption,
451 indexToEnsureInView, size):
452 """Adjusts the index of the current page if necessary: make sure the
453 current page is not after the end of the contents, and optionally go
454 to the page containing a specific item. There are no side effects but
455 the adjusted page index is returned.
456
457 @param page:
458 page number to use as the starting point
459 @param needNullSelectOption:
460 true if a null option should be shown before any other
461 options (takes up the first slot on the first page, not
462 counted in index)
463 @param indexToEnsureInView:
464 index of an item that should be included on the page (in
465 the data set, not counting the null item if any), -1 for
466 none
467 @param size:
468 number of items after filtering (not including the null
469 item, if any)
470 """
471 if indexToEnsureInView != -1:
472 if needNullSelectOption:
473 newPage = (indexToEnsureInView + 1) / self.pageLength
474 else:
475 newPage = indexToEnsureInView / self.pageLength
476
477 page = newPage
478
479
480 if (page * self.pageLength) > size:
481 if needNullSelectOption:
482 page = (size + 1) / self.pageLength
483 else:
484 page = size / self.pageLength
485
486 return page
487
488
490 """Filters the options in memory and returns the full filtered list.
491
492 This can be less efficient than using container filters, so use
493 L{getOptionsWithFilter} if possible (filterable container and suitable
494 item caption mode etc.).
495 """
496 if ((self._filterstring is None) or (self._filterstring == '')
497 or (self.FILTERINGMODE_OFF == self._filteringMode)):
498 self._prevfilterstring = None
499 self._filteredOptions = list(self.getItemIds())
500 return self._filteredOptions
501
502 if self._filterstring == self._prevfilterstring:
503 return self._filteredOptions
504
505 if ((self._prevfilterstring is not None)
506 and self._filterstring.startswith(self._prevfilterstring)):
507 items = self._filteredOptions
508 else:
509 items = self.getItemIds()
510 self._prevfilterstring = self._filterstring
511
512 self._filteredOptions = list()
513 for itemId in items:
514 caption = self.getItemCaption(itemId)
515 if (caption is None) or (caption == ''):
516 continue
517 else:
518 caption = caption.lower()
519
520 test = self._filteringMode
521 if test == self.FILTERINGMODE_CONTAINS:
522 if caption.find(self._filterstring) > -1:
523 self._filteredOptions.append(itemId)
524 elif test == self.FILTERINGMODE_STARTSWITH:
525 pass
526 else:
527 if caption.startswith(self._filterstring):
528 self._filteredOptions.append(itemId)
529
530 return self._filteredOptions
531
532
534 """Invoked when the value of a variable has changed.
535
536 @see: L{AbstractComponent.changeVariables}
537 """
538
539
540
541
542 if 'selected' in variables:
543 ka = variables.get('selected')
544
545 if self.isMultiSelect():
546
547
548
549
550
551 s = list()
552 for i in range(len(ka)):
553 idd = self.itemIdMapper.get(ka[i])
554 if (idd is not None) and self.containsId(idd):
555 s.append(idd)
556
557
558
559 visible = self.getVisibleItemIds()
560 if visible is not None:
561 newsel = self.getValue()
562
563 if newsel is None:
564 newsel = set()
565 else:
566 newsel = set(newsel)
567
568 newsel = newsel.difference(visible)
569 newsel = newsel.union(s)
570
571 self.setValue(newsel, True)
572 else:
573
574 if len(ka) == 0:
575
576 current = self.getValue()
577 visible = self.getVisibleItemIds()
578
579 if (visible is not None) and (current in visible):
580 self.setValue(None, True)
581 else:
582 idd = self.itemIdMapper.get(ka[0])
583
584 if ((idd is not None)
585 and (idd == self.getNullSelectionItemId())):
586 self.setValue(None, True)
587 else:
588 self.setValue(idd, True)
589
590 newFilter = variables.get('filter')
591 if newFilter is not None:
592
593 self._currentPage = int(variables.get('page'))
594 self._filterstring = newFilter
595 if self._filterstring is not None:
596 self._filterstring = self._filterstring.lower()
597
598 self.optionRepaint()
599 elif self.isNewItemsAllowed():
600
601 newitem = variables.get('newitem')
602 if (newitem is not None) and (len(newitem) > 0):
603 self.getNewItemHandler().addNewItem(newitem)
604
605 self._filterstring = None
606 self._prevfilterstring = None
607
608 if FocusEvent.EVENT_ID in variables:
609 self.fireEvent( FocusEvent(self) )
610
611 if BlurEvent.EVENT_ID in variables:
612 self.fireEvent( BlurEvent(self) )
613
614
616 super(Select, self).requestRepaint()
617 self._optionRequest = False
618 self._prevfilterstring = self._filterstring
619 self._filterstring = None
620
621
624
625
627 self._filteringMode = filteringMode
628
629
631 return self._filteringMode
632
633
635 """Note, one should use more generic setWidth(String) method instead
636 of this. This now days actually converts columns to width with em css
637 unit.
638
639 Sets the number of columns in the editor. If the number of columns is
640 set 0, the actual number of displayed columns is determined implicitly
641 by the adapter.
642
643 @deprecated:
644
645 @param columns:
646 the number of columns to set.
647 """
648 warn('deprecated', DeprecationWarning)
649
650 if columns < 0:
651 columns = 0
652
653 if self._columns != columns:
654 self._columns = columns
655 self.setWidth(columns, Select.UNITS_EM)
656 self.requestRepaint()
657
658
660 """@deprecated: see setter function
661 """
662 warn('see setter function', DeprecationWarning)
663 return self._columns
664
665
667 if (isinstance(listener, IBlurListener) and
668 (iface is None or issubclass(iface, IBlurListener))):
669 self.registerListener(BlurEvent.EVENT_ID,
670 BlurEvent, listener, IBlurListener.blurMethod)
671
672 if (isinstance(listener, IFocusListener) and
673 (iface is None or issubclass(iface, IFocusListener))):
674 self.registerListener(FocusEvent.EVENT_ID,
675 FocusEvent, listener, IFocusListener.focusMethod)
676
677 super(Select, self).addListener(listener, iface)
678
679
680 - def addCallback(self, callback, eventType=None, *args):
681 if eventType is None:
682 eventType = callback._eventType
683
684 if issubclass(eventType, BlurEvent):
685 self.registerCallback(BlurEvent, callback,
686 BlurEvent.EVENT_ID, *args)
687
688 elif issubclass(eventType, FocusEvent):
689 self.registerCallback(FocusEvent, callback,
690 FocusEvent.EVENT_ID, *args)
691 else:
692 super(Select, self).addCallback(callback, eventType, *args)
693
694
696 if (isinstance(listener, IBlurListener) and
697 (iface is None or issubclass(iface, IBlurListener))):
698 self.withdrawListener(BlurEvent.EVENT_ID, BlurEvent, listener)
699
700 if (isinstance(listener, IFocusListener) and
701 (iface is None or issubclass(iface, IFocusListener))):
702 self.withdrawListener(FocusEvent.EVENT_ID, FocusEvent, listener)
703
704 super(Select, self).removeListener(listener, iface)
705
706
719
720
722 """@deprecated: use L{ListSelect}, L{OptionGroup} or
723 L{TwinColSelect} instead
724 @see: L{AbstractSelect.setMultiSelect}
725 """
726 super(Select, self).setMultiSelect(multiSelect)
727
728
730 """@deprecated: use L{ListSelect}, L{OptionGroup} or
731 L{TwinColSelect} instead
732
733 @see: L{AbstractSelect.isMultiSelect}
734 """
735 return super(Select, self).isMultiSelect()
736
737
751
752
763