comparison src/browser/sat_browser/list_manager.py @ 736:fe3c2357a8c9

fixes/improve ListManager and contact group manager + better PEP-8 compliance
author souliane <souliane@mailoo.org>
date Thu, 19 Nov 2015 11:41:03 +0100
parents 9877607c719a
children 4545d48dee60
comparison
equal deleted inserted replaced
735:e4ae8e2b0afd 736:fe3c2357a8c9
16 16
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.log import getLogger 20 from sat.core.log import getLogger
21 from pyjamas.ui.DragHandler import DragHandler
21 log = getLogger(__name__) 22 log = getLogger(__name__)
23
24 from pyjamas.ui.ClickListener import ClickHandler
25 from pyjamas.ui.FocusListener import FocusHandler
26 from pyjamas.ui.ChangeListener import ChangeHandler
27 from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER
28 from pyjamas.ui.DragWidget import DragWidget
29 from pyjamas.ui.ListBox import ListBox
22 from pyjamas.ui.Button import Button 30 from pyjamas.ui.Button import Button
23 from pyjamas.ui.FlowPanel import FlowPanel 31 from pyjamas.ui.FlowPanel import FlowPanel
32 from pyjamas.ui.HorizontalPanel import HorizontalPanel
33 from pyjamas.ui.FlexTable import FlexTable
24 from pyjamas.ui.AutoComplete import AutoCompleteTextBox 34 from pyjamas.ui.AutoComplete import AutoCompleteTextBox
25 from pyjamas.ui.KeyboardListener import KEY_ENTER
26 from pyjamas.ui.DragWidget import DragWidget
27 from pyjamas.Timer import Timer
28 35
29 import base_panel 36 import base_panel
30 import base_widget 37 import base_widget
31 import libervia_widget 38 import libervia_widget
32 39
33 from sat_frontends.tools import jid 40 from sat_frontends.quick_frontend import quick_list_manager
34 41
35 42
36 unicode = str # FIXME: pyjamas workaround 43 unicode = str # FIXME: pyjamas workaround
37 44
38 # HTML content for the removal button (image or text) 45
39 REMOVE_BUTTON = '<span class="itemRemoveIcon">x</span>' 46 class ListItem(HorizontalPanel):
40 47 """This class implements a list item with auto-completion and a delete button."""
41 48
42 # FIXME: dirty method and magic string to fix ASAP 49 STYLE = {"listItem-box": "listItem-box",
43 def tryJID(obj): 50 "listItem-box-invalid": "listItem-box-invalid",
44 return jid.JID(obj) if (isinstance(obj, unicode) and not obj.startswith('@')) else obj 51 "listItem-button": "listItem-button",
45 52 }
46 53
47 class ListManager(object): 54 VALID = 1
48 """A base class to manage one or several lists of items.""" 55 INVALID = 2
49 56 DUPLICATE = 3
50 def __init__(self, container, keys=None, items=None, offsets=None, style=None): 57
51 """ 58 def __init__(self, listener=None, taglist=None, validate=None):
52 @param container (FlexTable): FlexTable parent widget 59 """
53 @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items 60
54 keys to their display config data. 61 @param listener (ListItemHandler): handler for the UI events
55 @param items (list): list of items 62 @param taglist (quick_list_manager.QuickTagList): list manager
56 @param offsets (dict): define widgets positions offsets within container: 63 @param validate (callable): method returning a bool to validate the entry
57 - "x_first": the x offset for the first widget's row on the grid 64 """
58 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined 65 HorizontalPanel.__init__(self)
59 - "y": the y offset for all widgets columns on the grid 66
60 @param style (dict): define CSS styles 67 self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"])
61 """ 68 self.remove_btn = Button('<span>x</span>', Visible=False)
62 self.container = container 69 self.remove_btn.setStyleName(self.STYLE["listItem-button"])
63 self.keys = {} if keys is None else keys 70 self.add(self.box)
64 self.items = [] if items is None else items 71 self.add(self.remove_btn)
65 self.items.sort() 72
66 73 if listener:
67 # store the list of items that are not assigned yet 74 self.box.addFocusListener(listener)
68 self.items_remaining = [item for item in self.items] 75 self.box.addChangeListener(listener)
69 self.items_remaining_sorted = True 76 self.box.addKeyboardListener(listener)
70 77 self.box.choices.addClickListener(listener)
71 self.offsets = {"x_first": 0, "x": 0, "y": 0} 78 self.remove_btn.addClickListener(listener)
72 if offsets is not None: 79
73 if "x" in offsets and "x_first" not in offsets: 80 self.taglist = taglist
74 offsets["x_first"] = offsets["x"] 81 self.validate = validate
75 self.offsets.update(offsets) 82 self.last_checked_value = ""
76 83 self.last_validity = self.VALID
77 self.style = {"keyItem": "itemKey",
78 "popupMenuItem": "itemKey",
79 "buttonCell": "itemButtonCell",
80 "keyPanel": "itemPanel",
81 "textBox": "itemTextBox",
82 "textBox-invalid": "itemTextBox-invalid",
83 "removeButton": "itemRemoveButton",
84 }
85 if style is not None:
86 self.style.update(style)
87
88 def createWidgets(self, title_format="%s"):
89 """Fill the container widget with one ListPanel per item key (some may be
90 hidden during the initialization).
91
92 @param title_format (unicode): format string for the title
93 """
94 self.children = {}
95 for key in self.keys:
96 self.addItemKey(key, title_format=title_format)
97
98 def addItemKey(self, key, data=None, title_format="%s"):
99 """Add to the container a Button and ListPanel for a new item key.
100
101 @param key (unicode): item key
102 @param data (dict{unicode: unicode}): config data
103 """
104 key_data = self.keys.setdefault(key, {})
105 if data is not None:
106 key_data.update(data)
107 key_data["title"] = key # copy the key to its associated sub-map
108
109 button = Button(title_format % key)
110 button.setStyleName(self.style["keyItem"])
111 if hasattr(key_data, "desc"):
112 button.setTitle(key_data["desc"])
113 if "optional" not in key_data:
114 key_data["optional"] = False
115 button.setVisible(not key_data["optional"])
116 y = len(self.children) + self.offsets["y"]
117 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
118
119 self.container.insertRow(y)
120 self.container.setWidget(y, x, button)
121 self.container.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
122
123 _child = ListPanel(self, key_data, self.style)
124 self.container.setWidget(y, x + 1, _child)
125
126 self.children[key] = {}
127 self.children[key]["button"] = button
128 self.children[key]["panel"] = _child
129
130 if hasattr(self, "popup_menu"):
131 # self.registerPopupMenuPanel has been called yet
132 self.popup_menu.registerClickSender(button)
133
134 def removeItemKey(self, key):
135 """Remove from the container a ListPanel representing an item key, and all
136 its associated data.
137
138 @param key (unicode): item key
139 """
140 items = self.children[key]["panel"].getItems()
141 (y, x) = self.container.getIndex(self.children[key]["button"])
142 self.container.removeRow(y)
143 del self.children[key]
144 del self.keys[key]
145 self.addToRemainingList(items)
146
147 def refresh(self, hide_everything=False):
148 """Set visible the sub-panels that are non optional or non empty, hide
149 the rest. Setting the attribute "hide_everything" to True you can also
150 hide everything.
151
152 @param hide_everything (boolean): set to True to hide everything
153 """
154 for key in self.children:
155 self.setItemPanelVisible(key, False)
156 if hide_everything:
157 return
158 for key, items in self.getItemsByKey().iteritems():
159 if len(items) > 0 or not self.keys[key]["optional"]:
160 self.setItemPanelVisible(key, True)
161
162 def setVisible(self, visible):
163 self.refresh(not visible)
164
165 def setItemPanelVisible(self, key, visible=True, sender=None):
166 """Set the item key's widgets visibility.
167
168 @param key (unicode): item key
169 @param visible (bool): set to True to display the widgets
170 @param sender
171 """
172 self.children[key]["button"].setVisible(visible)
173 self.children[key]["panel"].setVisible(visible)
174 84
175 @property 85 @property
176 def items_remaining(self): 86 def text(self):
177 """Return the unused items.""" 87 return self.box.getText()
178 if not self.items_remaining_sorted: 88
179 self.items_remaining.sort() 89 def setText(self, text):
180 self.items_remaining_sorted = True 90 """
181 return self.items_remaining 91 Set the text and refresh the Widget.
182 92
183 def setRemainingListUnsorted(self): 93 @param text (unicode): text to set
184 """Mark the list of unused items as being unsorted.""" 94 """
185 self.items_remaining_sorted = False 95 self.box.setText(text)
186 96 self.refresh()
187 def removeFromRemainingList(self, items): 97
188 """Remove some items from the list of unused items. 98 def refresh(self):
189 99 if self.last_checked_value == self.text:
190 @param items (list): items to be removed 100 return
191 """ 101
102 if self.taglist and self.last_checked_value:
103 self.taglist.untag([self.last_checked_value])
104
105 if self.validate: # if None, the state is always valid
106 self.last_validity = self.validate(self.text)
107
108 if self.last_validity == self.VALID:
109 self.box.removeStyleName(self.STYLE["listItem-box-invalid"])
110 elif self.last_validity == self.INVALID:
111 self.box.addStyleName(self.STYLE["listItem-box-invalid"])
112 elif self.last_validity == self.DUPLICATE:
113 self.remove_btn.click() # this may do more stuff then self.remove()
114 return
115
116 if self.taglist and self.text:
117 self.taglist.tag([self.text])
118 self.last_checked_value = self.text
119 self.remove_btn.setVisible(len(self.text) > 0)
120
121 def setFocus(self, focused):
122 self.box.setFocus(focused)
123
124 def remove(self):
125 """Remove the list item from its parent."""
126 self.removeFromParent()
127
128 if self.taglist and self.text: # this must be done after the widget has been removed
129 self.taglist.untag([self.text])
130
131
132 class DraggableListItem(ListItem, DragWidget):
133 """This class is like ListItem, but in addition it can be dragged."""
134
135 def __init__(self, listener=None, taglist=None, validate=None):
136 """
137
138 @param listener (ListItemHandler): handler for the UI events
139 @param taglist (quick_list_manager.QuickTagList): list manager
140 @param validate (callable): method returning a bool to validate the entry
141 """
142 ListItem.__init__(self, listener, taglist, validate)
143 DragWidget.__init__(self)
144 self.addDragListener(listener)
145
146
147 def onDragStart(self, event):
148 """The user starts dragging the item."""
149 self.box.setSelectionRange(len(self.text), 0)
150
151 dt = event.dataTransfer
152 dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX"))
153 dt.setDragImage(self.box.getElement(), 15, 15)
154
155
156 class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler):
157 """Implements basic handlers for the ListItem events."""
158
159 last_item = None # the last item is an empty text box for user input
160
161 def __init__(self, manager, key):
162 ClickHandler.__init__(self)
163 FocusHandler.__init__(self)
164 ChangeHandler.__init__(self)
165 KeyboardHandler.__init__(self)
166 self.manager = manager
167 self.key = key
168
169 def addItem(self, item):
170 raise NotImplementedError
171
172 def removeItem(self, item):
173 raise NotImplementedError
174
175 def onClick(self, sender):
176 """The remove button or a suggested completion item has been clicked."""
177 #log.debug("onClick sender type: %s" % type(sender))
178 if isinstance(sender, Button):
179 item = sender.getParent()
180 self.removeItem(item)
181 elif isinstance(sender, ListBox):
182 # this is called after onChange when you click a suggested item, and now we get the final value
183 textbox = sender._clickListeners[0]
184 self.checkValue(textbox)
185 else:
186 raise AssertionError
187
188 def onFocus(self, sender):
189 """The text box has the focus."""
190 #log.debug("onFocus sender type: %s" % type(sender))
191 assert isinstance(sender, AutoCompleteTextBox)
192 sender.setCompletionItems(self.manager.untagged)
193
194 def onKeyUp(self, sender, keycode, modifiers):
195 """The text box is being modified - or ENTER key has been pressed."""
196 # this is called after onChange when you press ENTER, and now we get the final value
197 #log.debug("onKeyUp sender type: %s" % type(sender))
198 assert isinstance(sender, AutoCompleteTextBox)
199 if keycode == KEY_ENTER:
200 self.checkValue(sender)
201
202 def onChange(self, sender):
203 """The text box has been changed by the user."""
204 # this is called before the completion when you press ENTER or click a suggest item
205 #log.debug("onChange sender type: %s" % type(sender))
206 assert isinstance(sender, AutoCompleteTextBox)
207 self.checkValue(sender)
208
209 def checkValue(self, textbox):
210 """Internal handler to call when a new value is submitted by the user."""
211 item = textbox.getParent()
212 if item.text == item.last_checked_value:
213 # this method has already been called (by self.onChange) and there's nothing new
214 return
215 item.refresh()
216 item.box.setSelectionRange(len(item.text), 0)
217 if item == self.last_item and item.last_validity == ListItem.VALID and item.text:
218 self.addItem()
219
220 class DraggableListItemHandler(ListItemHandler, DragHandler):
221 """Implements basic handlers for the DraggableListItem events."""
222
223 def __init__(self, manager, key):
224 ListItemHandler.__init__(self, manager, key)
225 DragHandler.__init__(self)
226
227 def onDragStart(self, event):
228 """The user starts dragging the item."""
229 self.manager.drop_target = None
230
231 def onDragEnd(self, event):
232 """The user dropped the list item."""
233 text, dummy = libervia_widget.eventGetData(event)
234 target = self.manager.drop_target # self or another ListPanel
235 if text == "" or target is None:
236 return
237 if target != self: # move the item from self to target
238 target.addItem(text)
239 self.removeItem(self.getItem(text))
240
241
242 class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell):
243 """Implements a list of items."""
244 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented:
245 # - it can not be used with pyjamas.ui.Label
246 # - FlowPanel.insert doesn't work
247
248 STYLE = {"listPanel": "listPanel"}
249
250 def __init__(self, manager, key, items):
251 """Initialization with a button for the list name (key) and a DraggableListItem.
252
253 @param manager (ListManager)
254 @param key (unicode): list name
255 @param items (list): items to append
256 """
257 FlowPanel.__init__(self)
258 DraggableListItemHandler.__init__(self, manager, key)
259 libervia_widget.DropCell.__init__(self, None)
260 self.addStyleName(self.STYLE["listPanel"])
261 self.manager = manager
262 items.sort()
263 self.addItem()
192 for item in items: 264 for item in items:
193 if item in self.items_remaining: 265 self.addItem(unicode(item))
194 self.items_remaining.remove(item)
195
196 def addToRemainingList(self, items, ignore_key=None):
197 """Add some items to the list of unused items. Check first if the
198 items are really not used in any ListPanel.
199
200 @param items (list): items to be removed
201 @param ignore_key (unicode): item key to be ignored while checking
202 """
203 items_assigned = set()
204 for key, current_items in self.getItemsByKey().iteritems():
205 if ignore_key is not None and key == ignore_key:
206 continue
207 items_assigned.update(current_items)
208 for item in items:
209 if item not in self.items or item in self.items_remaining or item in items_assigned:
210 continue
211 self.items_remaining.append(item)
212 self.setRemainingListUnsorted()
213
214 def resetItems(self, data={}):
215 """Repopulate all the lists (one per item key) with the given items.
216
217 @param data (dict{unicode: list}): dict binding items keys to items.
218 """
219 for key in self.keys:
220 if key in data:
221 self.children[key]["panel"].resetItems(data[key])
222 else:
223 self.children[key]["panel"].resetItems([])
224 self.refresh()
225
226 def getItemsByKey(self):
227 """Get all the items by key.
228
229 @return: dict{unicode: set}
230 """
231 return {key: self.children[key]["panel"].getItems() for key in self.children}
232
233 def getKeysByItem(self):
234 """Get all the keys by item.
235
236 @return: dict{object: set(unicode)}
237 """
238 result = {}
239 for key in self.children:
240 for item in self.children[key]["panel"].getItems():
241 result.setdefault(item, set()).add(key)
242 return result
243
244 def registerPopupMenuPanel(self, entries, hide, callback):
245 """Register a popup menu panel for the item keys buttons.
246
247 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries
248 @param hide (callable): method to call in order to know if a menu item
249 should be hidden from the menu. Takes in the button widget and the
250 item key and returns a boolean.
251 @param callback (callable): common callback for all menu items, takes in
252 the button widget and the item key.
253 """
254 self.popup_menu = base_panel.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
255
256
257 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget):
258 """A draggable AutoCompleteTextBox which is used for representing an item."""
259
260 def __init__(self, list_panel, event_cbs, style):
261 """
262
263 @param list_panel (ListPanel)
264 @param event_cbs (list[callable])
265 @param style (dict)
266 """
267 AutoCompleteTextBox.__init__(self)
268 DragWidget.__init__(self)
269 self.list_panel = list_panel
270 self.event_cbs = event_cbs
271 self.style = style
272 self.addStyleName(style["textBox"])
273 self.reset()
274
275 # Parent classes already init self as an handler for these events
276 self.addMouseListener(self)
277 self.addFocusListener(self)
278 self.addChangeListener(self)
279
280 def onDragStart(self, event):
281 """The user starts dragging the text box."""
282 self.list_panel.manager.target_drop_cell = None
283 self.setSelectionRange(len(self.getText()), 0)
284
285 dt = event.dataTransfer
286 dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX"))
287 dt.setDragImage(self.getElement(), 15, 15)
288
289 def onDragEnd(self, event):
290 """The user dropped the text box."""
291 target = self.list_panel.manager.target_drop_cell # parent or another ListPanel
292 if self.getText() == "" or target is None:
293 return
294 self.event_cbs["drop"](self, target)
295
296 def onClick(self, sender):
297 """The choices list is clicked"""
298 assert sender == self.choices
299 AutoCompleteTextBox.onClick(self, sender)
300 self.validate()
301
302 def onChange(self, sender):
303 """The list selection or the text has been changed"""
304 assert sender == self.choices or sender == self
305 if sender == self.choices:
306 AutoCompleteTextBox.onChange(self, sender)
307 self.validate()
308
309 def onKeyUp(self, sender, keycode, modifiers):
310 """Listen for key stroke"""
311 assert sender == self
312 AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
313 if keycode == KEY_ENTER:
314 self.validate()
315
316 def onMouseMove(self, sender):
317 """Mouse enters the area of a DragAutoCompleteTextBox."""
318 assert sender == self
319 if hasattr(sender, "remove_btn"):
320 sender.remove_btn.setVisible(True)
321
322 def onMouseLeave(self, sender):
323 """Mouse leaves the area of a DragAutoCompleteTextBox."""
324 assert sender == self
325 if hasattr(sender, "remove_btn"):
326 Timer(1500, lambda timer: sender.remove_btn.setVisible(False))
327
328 def onFocus(self, sender):
329 """The DragAutoCompleteTextBox has the focus."""
330 assert sender == self
331 # FIXME: this raises runtime JS error "Permission denied to access property..." when you drag the object
332 #sender.setSelectionRange(0, len(sender.getText()))
333 sender.event_cbs["focus"](sender)
334
335 def reset(self):
336 """Reset the text box"""
337 self.setText("")
338 self.setValid()
339
340 def setValid(self, valid=True):
341 """Change the style according to the text validity."""
342 if self.getText() == "":
343 valid = True
344 if valid:
345 self.removeStyleName(self.style["textBox-invalid"])
346 else:
347 self.addStyleName(self.style["textBox-invalid"])
348 self.valid = valid
349
350 def validate(self):
351 """Check if the text is valid, update the style."""
352 self.setSelectionRange(len(self.getText()), 0)
353 self.event_cbs["validate"](self)
354
355 def setRemoveButton(self):
356 """Add the remove button after the text box."""
357
358 def remove_cb(sender):
359 """Callback for the button to remove this item."""
360 self.list_panel.remove(self)
361 self.list_panel.remove(self.remove_btn)
362 self.event_cbs["remove"](self)
363
364 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
365 self.remove_btn.setStyleName(self.style["removeButton"])
366 self.list_panel.add(self.remove_btn)
367
368 def removeOrReset(self):
369 """Remove the text box if the remove button exists, or reset the text box."""
370 if hasattr(self, "remove_btn"):
371 self.remove_btn.click()
372 else:
373 self.reset()
374
375
376 VALID = 1
377 INVALID = 2
378 DELETE = 3
379
380
381 class ListPanel(FlowPanel, libervia_widget.DropCell):
382 """Panel used for listing items sharing the same key. The key is showed as
383 a Button to which you can bind a popup menu and the items are represented
384 with a sequence of DragAutoCompleteTextBox."""
385 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label
386
387 def __init__(self, manager, data, style={}):
388 """Initialization with a button and a DragAutoCompleteTextBox.
389
390 @param manager (ListManager)
391 @param data (dict{unicode: unicode})
392 @param style (dict{unicode: unicode})
393 """
394 FlowPanel.__init__(self, Visible=(False if data["optional"] else True))
395
396 def setTargetDropCell(host, item):
397 self.manager.target_drop_cell = self
398 266
399 # FIXME: dirty magic strings '@' and '@@' 267 # FIXME: dirty magic strings '@' and '@@'
400 drop_cbs = {"GROUP": lambda host, item: self.addItem("@%s" % item), 268 self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s),
401 "CONTACT": lambda host, item: self.addItem(tryJID(item)), 269 "CONTACT": lambda host, item_s: self.addItem(item_s),
402 "CONTACT_TITLE": lambda host, item: self.addItem('@@'), 270 "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'),
403 "CONTACT_TEXTBOX": setTargetDropCell 271 "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self),
404 } 272 }
405 libervia_widget.DropCell.__init__(self, None)
406 self.drop_keys = drop_cbs
407 self.style = style
408 self.addStyleName(self.style["keyPanel"])
409 self.manager = manager
410 self.key = data["title"]
411 self._addTextBox()
412 273
413 def onDrop(self, event): 274 def onDrop(self, event):
275 """Something has been dropped in this ListPanel"""
414 try: 276 try:
415 libervia_widget.DropCell.onDrop(self, event) 277 libervia_widget.DropCell.onDrop(self, event)
416 except base_widget.NoLiberviaWidgetException: 278 except base_widget.NoLiberviaWidgetException:
417 pass 279 pass
418 280
419 def _addTextBox(self, switchPrevious=False): 281 def getItem(self, text):
420 """Add an empty text box to the last position. 282 """Get an item from its text.
421 283
422 @param switchPrevious (bool): if True, simulate an insertion before the 284 @param text(unicode): item text
423 current last textbox by switching the texts and valid states 285 """
424 @return: an DragAutoCompleteTextBox, the created text box or the 286 for child in self.getChildren():
425 previous one if switchPrevious is True. 287 if child.text == text:
426 """ 288 return child
427 if hasattr(self, "_last_textbox"): 289 return None
428 if self._last_textbox.getText() == "": 290
429 return 291 def getItems(self):
430 self._last_textbox.setRemoveButton() 292 """Get the non empty items.
431 else: 293
432 switchPrevious = False 294 @return list(unicode)
433 295 """
434 def focus_cb(sender): 296 return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text]
435 if sender != self._last_textbox: 297
436 # save the current value before it's being modified 298 def validateItem(self, text):
437 self.manager.addToRemainingList([tryJID(sender.getText())], ignore_key=self.key) 299 """Return validation code after the item has been changed.
438 300
439 items = [unicode(item) for item in self.manager.items_remaining] 301 @param text (unicode): item text to check
440 sender.setCompletionItems(items)
441
442 def add_cb(sender):
443 self.addItem(tryJID(sender.getText()), sender)
444
445 def remove_cb(sender):
446 """Callback for the button to remove this item."""
447 self.manager.addToRemainingList([tryJID(sender.getText())])
448 self.manager.setRemainingListUnsorted()
449 self._last_textbox.setFocus(True)
450
451 def drop_cb(sender, target):
452 """Callback when the textbox is drag-n-dropped."""
453 list_panel = sender.list_panel
454 if target != list_panel and target.addItem(tryJID(sender.getText())):
455 sender.removeOrReset()
456 else:
457 list_panel.manager.removeFromRemainingList([tryJID(sender.getText())])
458
459 events_cbs = {"focus": focus_cb, "validate": add_cb, "remove": remove_cb, "drop": drop_cb}
460 textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
461 self.add(textbox)
462 if switchPrevious:
463 textbox.setText(self._last_textbox.getText())
464 textbox.setValid(self._last_textbox.valid)
465 self._last_textbox.reset()
466 previous = self._last_textbox
467 self._last_textbox = textbox
468 return previous if switchPrevious else textbox
469
470 def _checkItem(self, item, modify):
471 """
472 @param item (object): the item to check
473 @param modify (bool): True if the item is being modified
474 @return: int value defined by one of these constants: 302 @return: int value defined by one of these constants:
475 - VALID if the item is valid 303 - VALID if the item is valid
476 - INVALID if the item is not valid but can be displayed 304 - INVALID if the item is not valid but can be displayed
477 - DELETE if the item should not be displayed at all 305 - DUPLICATE if the item is a duplicate
478 """ 306 """
479 def count(list_, item): 307 def count(list_, item): # XXX: list.count in not implemented by pyjamas
480 # XXX: list.count in not implemented by pyjamas
481 return len([elt for elt in list_ if elt == item]) 308 return len([elt for elt in list_ if elt == item])
482 309
483 if not item: 310 if count(self.getItems(), text) > 1:
484 return DELETE 311 return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion
485 if count(self.getItems(), item) > (1 if modify else 0): 312 return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID
486 return DELETE 313
487 return VALID if item in self.manager.items else INVALID 314 def addItem(self, text=""):
488 315 """Add an item.
489 def addItem(self, item, sender=None): 316
490 """Try to add an item. It will be added if it's a valid one. 317 @param text (unicode): text to be set.
491 318 @return: True if the item has been really added or merged.
492 @param item (object): item to be added 319 """
493 @param (DragAutoCompleteTextBox): widget triggering the event 320 if text in self.getItems(): # avoid duplicate in the same list
494 @param sender: if True, the item will be "written" to the last textbox 321 return
495 and a new text box will be added afterward. 322
496 @return: True if the item has been added. 323 item = DraggableListItem(self, self.manager, self.validateItem)
497 """ 324 self.add(item)
498 valid = self._checkItem(item, sender is not None) 325
499 item_s = unicode(item) 326 if self.last_item:
500 if sender is None: 327 if self.last_item.last_validity == ListItem.INVALID:
501 # method has been called not to modify but to add an item 328 # switch the two values so that the invalid one stays in last position
502 if valid == VALID: 329 item.setText(self.last_item.text)
503 # eventually insert before the last textbox if it's not empty 330 self.last_item.setText(text)
504 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox 331 elif not self.last_item.text:
505 sender.setText(item_s) 332 # copy the new value to previous empty item
506 else: 333 self.last_item.setText(text)
507 sender.setValid(valid == VALID) 334 else: # first item of the list, or previous last item has been deleted
508 if valid != VALID: 335 item.setText(text)
509 if sender is not None and valid == DELETE: 336
510 sender.removeOrReset() 337 self.last_item = item
511 return False 338 self.last_item.setFocus(True)
512 if sender == self._last_textbox: 339
513 self._addTextBox() 340 def removeItem(self, item):
514 sender.setVisibleLength(len(item_s)) 341 """Remove an item.
515 self.manager.removeFromRemainingList([item]) 342
516 self._last_textbox.setFocus(True) 343 @param item(DraggableListItem): item to remove
517 return True 344 """
518 345 if item == self.last_item:
519 def emptyItems(self): 346 self.addItem("")
520 """Empty the list of items.""" 347 item.remove() # this also updates the taglist
521 for child in self.getChildren(): 348
522 if hasattr(child, "remove_btn"): 349
523 child.remove_btn.click() 350 class ListManager(FlexTable, quick_list_manager.QuickTagList):
524 351 """Implements a table to manage one or several lists of items."""
525 def resetItems(self, items): 352
526 """Repopulate the items. 353 STYLE = {"listManager-button": "group",
527 354 "listManager-button-cell": "listManager-button-cell",
528 @param items (list): the items to be listed. 355 }
529 """ 356
530 self.emptyItems() 357 def __init__(self, data=None, items=None):
531 if isinstance(items, set): 358 """
532 items = list(items) 359 @param data (dict{unicode: list}): dict binding keys to tagged items.
533 items.sort() 360 @param items (list): full list of items (tagged and untagged)
534 for item in items: 361 """
535 self.addItem(item) 362 FlexTable.__init__(self, Width="100%")
536 363 quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items])
537 def getItems(self): 364 self.lists = {}
538 """Get the listed items. 365
539 366 if data:
540 @return: set""" 367 for key, items in data.iteritems():
541 items = set() 368 self.addList(key, [unicode(item) for item in items])
542 for widget in self.getChildren(): 369
543 if isinstance(widget, DragAutoCompleteTextBox) and widget.getText() != "": 370 def addList(self, key, items=None):
544 items.add(tryJID(widget.getText())) 371 """Add a Button and ListPanel for a new list.
545 return items 372
373 @param key (unicode): list name
374 @param items (list): items to append to the new list
375 """
376 if key in self.lists:
377 return
378
379 if items is None:
380 items = []
381
382 self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]),
383 "panel": ListPanel(self, key, items)}
384
385 y, x = len(self.lists), 0
386 self.insertRow(y)
387 self.setWidget(y, x, self.lists[key]["button"])
388 self.setWidget(y, x + 1, self.lists[key]["panel"])
389 self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"])
390
391 try:
392 self.popup_menu.registerClickSender(self.lists[key]["button"])
393 except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet
394 pass
395
396 def removeList(self, key):
397 """Remove a ListPanel from this manager.
398
399 @param key (unicode): list name
400 """
401 items = self.lists[key]["panel"].getItems()
402 (y, x) = self.getIndex(self.lists[key]["button"])
403 self.removeRow(y)
404 del self.lists[key]
405 self.untag(items)
406
407 def untag(self, items):
408 """Untag some items.
409
410 Check first if the items are not used in any panel.
411
412 @param items (list): items to be removed
413 """
414 items_assigned = set()
415 for values in self.getItemsByKey().itervalues():
416 items_assigned.update(values)
417 quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned])
418
419 def getItemsByKey(self):
420 """Get the items grouped by list name.
421
422 @return dict{unicode: list}
423 """
424 return {key: self.lists[key]["panel"].getItems() for key in self.lists}
425
426 def getKeysByItem(self):
427 """Get the keys groups by item.
428
429 @return dict{object: set(unicode)}
430 """
431 result = {}
432 for key in self.lists:
433 for item in self.lists[key]["panel"].getItems():
434 result.setdefault(item, set()).add(key)
435 return result
436
437 def registerPopupMenuPanel(self, entries, callback):
438 """Register a popup menu panel for the list names' buttons.
439
440 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries
441 @param callback (callable): common callback for all menu items, arguments are:
442 - button widget
443 - list name (item key)
444 """
445 self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback)
446 for key in self.lists: # register click sender for already existing lists
447 self.popup_menu.registerClickSender(self.lists[key]["button"])