comparison browser_side/list_manager.py @ 263:d3c734669577

browser_side: improvements for lists and contact groups manager: - use DockPanel to deal with UI problems - fixed issues with the autocomplete list - avoid duplicate contacts in a contact list - signal invalid contacts with a red border - check for invalid contacts in the form before saving - better genericity for the class DragAutoCompleteTextBox
author souliane <souliane@mailoo.org>
date Mon, 11 Nov 2013 12:48:33 +0100
parents 0e7f3944bd27
children 2d6bd975a72d
comparison
equal deleted inserted replaced
262:30c01671e338 263:d3c734669577
26 from pyjamas.ui.AutoComplete import AutoCompleteTextBox 26 from pyjamas.ui.AutoComplete import AutoCompleteTextBox
27 from pyjamas.ui.Label import Label 27 from pyjamas.ui.Label import Label
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel 28 from pyjamas.ui.HorizontalPanel import HorizontalPanel
29 from pyjamas.ui.VerticalPanel import VerticalPanel 29 from pyjamas.ui.VerticalPanel import VerticalPanel
30 from pyjamas.ui.DialogBox import DialogBox 30 from pyjamas.ui.DialogBox import DialogBox
31 from pyjamas.ui.KeyboardListener import KEY_ENTER, KeyboardHandler 31 from pyjamas.ui.KeyboardListener import KEY_ENTER
32 from pyjamas.ui.MouseListener import MouseHandler 32 from pyjamas.ui.MouseListener import MouseHandler
33 from pyjamas.ui.FocusListener import FocusHandler 33 from pyjamas.ui.FocusListener import FocusHandler
34 from pyjamas.ui.DropWidget import DropWidget 34 from pyjamas.ui.DropWidget import DropWidget
35 from pyjamas.ui.DragWidget import DragWidget 35 from pyjamas.ui.DragWidget import DragWidget
36
37 from pyjamas.Timer import Timer 36 from pyjamas.Timer import Timer
38 from pyjamas import DOM 37 from pyjamas import DOM
39
40 import panels 38 import panels
41 from pyjamas.ui import FocusListener, KeyboardListener, MouseListener, Event 39
42 40
43 # HTML content for the removal button (image or text) 41 # HTML content for the removal button (image or text)
44 REMOVE_BUTTON = '<span class="richTextRemoveIcon">x</span>' 42 REMOVE_BUTTON = '<span class="richTextRemoveIcon">x</span>'
45 43
46 # Item to be considered for an empty list box selection. 44 # Item to be considered for an empty list box selection.
59 @param offsets: dict to set widget positions offset within parent 57 @param offsets: dict to set widget positions offset within parent
60 - "x_first": the x offset for the first widget's row on the grid 58 - "x_first": the x offset for the first widget's row on the grid
61 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined 59 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
62 - "y": the y offset for all widgets columns on the grid 60 - "y": the y offset for all widgets columns on the grid
63 """ 61 """
64 self.host = parent.host
65 self._parent = parent 62 self._parent = parent
66 if isinstance(keys_dict, set) or isinstance(keys_dict, list): 63 if isinstance(keys_dict, set) or isinstance(keys_dict, list):
67 tmp = {} 64 tmp = {}
68 for key in keys_dict: 65 for key in keys_dict:
69 tmp[key] = {} 66 tmp[key] = {}
89 "popupMenuItem": "recipientTypeItem", 86 "popupMenuItem": "recipientTypeItem",
90 "buttonCell": "recipientButtonCell", 87 "buttonCell": "recipientButtonCell",
91 "dragoverPanel": "dragover-recipientPanel", 88 "dragoverPanel": "dragover-recipientPanel",
92 "keyPanel": "recipientPanel", 89 "keyPanel": "recipientPanel",
93 "textBox": "recipientTextBox", 90 "textBox": "recipientTextBox",
91 "textBox-invalid": "recipientTextBox-invalid",
94 "removeButton": "recipientRemoveButton", 92 "removeButton": "recipientRemoveButton",
95 } 93 }
96 self.style.update(style) 94 self.style.update(style)
97 95
98 def createWidgets(self, title_format="%s"): 96 def createWidgets(self, title_format="%s"):
99 """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" 97 """Fill the parent grid with all the widgets (some may be hidden during the initialization)."""
100 self.__children = {} 98 self.__children = {}
101 for key in self.__keys_dict: 99 for key in self.__keys_dict:
102 self.addContactKey(key, title_format) 100 self.addContactKey(key, title_format=title_format)
103 101
104 def addContactKey(self, key, dict_={}, title_format="%s"): 102 def addContactKey(self, key, dict_={}, title_format="%s"):
105 if key not in self.__keys_dict: 103 if key not in self.__keys_dict:
106 self.__keys_dict[key] = dict_ 104 self.__keys_dict[key] = dict_
107 # copy the key to its associated sub-map 105 # copy the key to its associated sub-map
108 self.__keys_dict[key]["title"] = key 106 self.__keys_dict[key]["title"] = key
109 self._addChild(self.__keys_dict[key], title_format) 107 self._addChild(self.__keys_dict[key], title_format)
108
109 def removeContactKey(self, key):
110 """Remove a list panel and all its associated data."""
111 contacts = self.__children[key]["panel"].getContacts()
112 (y, x) = self._parent.getIndex(self.__children[key]["button"])
113 self._parent.removeRow(y)
114 del self.__children[key]
115 del self.__keys_dict[key]
116 self.addToRemainingList(contacts)
110 117
111 def _addChild(self, entry, title_format): 118 def _addChild(self, entry, title_format):
112 """Add a button and FlowPanel for the corresponding map entry.""" 119 """Add a button and FlowPanel for the corresponding map entry."""
113 button = Button(title_format % entry["title"]) 120 button = Button(title_format % entry["title"])
114 button.setStyleName(self.style["keyItem"]) 121 button.setStyleName(self.style["keyItem"])
118 entry["optional"] = False 125 entry["optional"] = False
119 button.setVisible(not entry["optional"]) 126 button.setVisible(not entry["optional"])
120 y = len(self.__children) + self.offsets["y"] 127 y = len(self.__children) + self.offsets["y"]
121 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] 128 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
122 129
130 self._parent.insertRow(y)
123 self._parent.setWidget(y, x, button) 131 self._parent.setWidget(y, x, button)
124 self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) 132 self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
125 133
126 _child = ListPanel(self, entry, self.style) 134 _child = ListPanel(self, entry, self.style)
127 self._parent.setWidget(y, x + 1, _child) 135 self._parent.setWidget(y, x + 1, _child)
171 179
172 def setRemainingListUnsorted(self): 180 def setRemainingListUnsorted(self):
173 """Mark a change (deletion) so the list will be sorted before it's used.""" 181 """Mark a change (deletion) so the list will be sorted before it's used."""
174 self.__remaining_list_sorted = False 182 self.__remaining_list_sorted = False
175 183
176 def removeFromRemainingList(self, contact_): 184 def removeFromRemainingList(self, contacts):
177 """Remove an available contact after it has been added to a sub-panel.""" 185 """Remove contacts after they have been added to a sub-panel."""
178 if contact_ in self.__remaining_list: 186 if not isinstance(contacts, list):
179 self.__remaining_list.remove(contact_) 187 contacts = [contacts]
180 188 for contact_ in contacts:
181 def addToRemainingList(self, contact_): 189 if contact_ in self.__remaining_list:
182 """Add a contact after it has been removed from a sub-panel.""" 190 self.__remaining_list.remove(contact_)
183 if contact_ not in self.__list or contact_ in self.__remaining_list: 191
184 return 192 def addToRemainingList(self, contacts, ignore_key=None):
185 self.__remaining_list.append(contact_) 193 """Add contacts after they have been removed from a sub-panel."""
186 self.__sort_remaining_list = True 194 if not isinstance(contacts, list):
195 contacts = [contacts]
196 assigned_contacts = set()
197 assigned_map = self.getContacts()
198 for key_ in assigned_map.keys():
199 if ignore_key is not None and key_ == ignore_key:
200 continue
201 assigned_contacts.update(assigned_map[key_])
202 for contact_ in contacts:
203 if contact_ not in self.__list or contact_ in self.__remaining_list:
204 continue
205 if contact_ in assigned_contacts:
206 continue # the contact is assigned somewhere else
207 self.__remaining_list.append(contact_)
208 self.setRemainingListUnsorted()
187 209
188 def setContacts(self, _map={}): 210 def setContacts(self, _map={}):
189 """Set the contacts for each contact key.""" 211 """Set the contacts for each contact key."""
190 for key in self.__keys_dict: 212 for key in self.__keys_dict:
191 if key in _map: 213 if key in _map:
200 _map = {} 222 _map = {}
201 for key in self.__children: 223 for key in self.__children:
202 _map[key] = self.__children[key]["panel"].getContacts() 224 _map[key] = self.__children[key]["panel"].getContacts()
203 return _map 225 return _map
204 226
205 def setTargetDropCell(self, panel): 227 @property
206 """Used to drag and drop the contacts from one panel to another.""" 228 def target_drop_cell(self):
207 self._target_drop_cell = panel 229 """@return: the panel where something has been dropped."""
208 230 return self.target_drop_cell
209 def getTargetDropCell(self): 231
210 """Used to drag and drop the contacts from one panel to another.""" 232 @target_drop_cell.setter
211 return self._target_drop_cell 233 def target_drop_cell(self, target_drop_cell):
234 """@param: target_drop_cell: the panel where something has been dropped."""
235 self.target_drop_cell = target_drop_cell
212 236
213 def registerPopupMenuPanel(self, entries, hide, callback): 237 def registerPopupMenuPanel(self, entries, hide, callback):
214 "Register a popup menu panel that will be bound to all contact keys elements." 238 "Register a popup menu panel that will be bound to all contact keys elements."
215 self.popup_menu = panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, item_style=self.style["popupMenuItem"]) 239 self.popup_menu = panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, item_style=self.style["popupMenuItem"])
216 240
217 241
218 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget, MouseHandler): 242 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget, MouseHandler, FocusHandler):
219 """A draggable AutoCompleteTextBox which is used for representing a contact. 243 """A draggable AutoCompleteTextBox which is used for representing a contact.
220 This class is NOT generic because of the onDragEnd method which call methods 244 This class is NOT generic because of the onDragEnd method which call methods
221 from ListPanel. It's probably not reusable for another scenario. 245 from ListPanel. It's probably not reusable for another scenario.
222 """ 246 """
223 247
224 def __init__(self): 248 def __init__(self, parent, event_cbs, style):
225 AutoCompleteTextBox.__init__(self) 249 AutoCompleteTextBox.__init__(self)
226 DragWidget.__init__(self) 250 DragWidget.__init__(self)
251 self._parent = parent
252 self.event_cbs = event_cbs
253 self.style = style
227 self.addMouseListener(self) 254 self.addMouseListener(self)
255 self.addFocusListener(self)
256 self.addChangeListener(self)
257 self.addStyleName(style["textBox"])
258 self.reset()
259
260 def reset(self):
261 self.setText("")
262 self.setValid()
263
264 def setValid(self, valid=True):
265 if self.getText() == "":
266 valid = True
267 if valid:
268 self.removeStyleName(self.style["textBox-invalid"])
269 else:
270 self.addStyleName(self.style["textBox-invalid"])
271 self.valid = valid
228 272
229 def onDragStart(self, event): 273 def onDragStart(self, event):
230 dt = event.dataTransfer 274 dt = event.dataTransfer
231 # The group prefix "@" is already in text so we use only the "CONTACT" type 275 # The group prefix "@" is already in text so we use only the "CONTACT" type
232 dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX")) 276 dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX"))
277 self.setSelectionRange(len(self.getText()), 0)
233 278
234 def onDragEnd(self, event): 279 def onDragEnd(self, event):
235 if self.getText() == "": 280 if self.getText() == "":
236 return 281 return
237 # get the ListPanel containing self 282 target = self._parent.target_drop_cell # parent or another ListPanel
238 parent = self.getParent() 283 self.event_cbs["drop"](self, target)
239 while parent is not None and not isinstance(parent, ListPanel): 284
240 parent = parent.getParent() 285 def setRemoveButton(self):
241 if parent is None: 286
242 return 287 def remove_cb(sender):
243 # it will return parent again or another ListPanel 288 """Callback for the button to remove this contact."""
244 target = parent.getTargetDropCell() 289 self._parent.remove(self)
245 if target == parent: 290 self._parent.remove(self.remove_btn)
246 return 291 self.event_cbs["remove"](self)
247 target.addContact(self.getText()) 292
293 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
294 self.remove_btn.setStyleName(self.style["removeButton"])
295 self._parent.add(self.remove_btn)
296
297 def removeOrReset(self):
248 if hasattr(self, "remove_btn"): 298 if hasattr(self, "remove_btn"):
249 # self is not the last textbox, just remove it
250 self.remove_btn.click() 299 self.remove_btn.click()
251 else: 300 else:
252 # reset the value of the last textbox 301 self.reset()
253 self.setText("")
254 302
255 def onMouseMove(self, sender): 303 def onMouseMove(self, sender):
256 """Mouse enters the area of a DragAutoCompleteTextBox.""" 304 """Mouse enters the area of a DragAutoCompleteTextBox."""
257 if hasattr(sender, "remove_btn"): 305 if hasattr(sender, "remove_btn"):
258 sender.remove_btn.setVisible(True) 306 sender.remove_btn.setVisible(True)
259 307
260 def onMouseLeave(self, sender): 308 def onMouseLeave(self, sender):
261 """Mouse leaves the area of a DragAutoCompleteTextBox.""" 309 """Mouse leaves the area of a DragAutoCompleteTextBox."""
262 if hasattr(sender, "remove_btn"): 310 if hasattr(sender, "remove_btn"):
263 Timer(1500, lambda: sender.remove_btn.setVisible(False)) 311 Timer(1500, lambda: sender.remove_btn.setVisible(False))
312
313 def onFocus(self, sender):
314 sender.setSelectionRange(0, len(self.getText()))
315 self.event_cbs["focus"](sender)
316
317 def validate(self):
318 self.setSelectionRange(len(self.getText()), 0)
319 self.event_cbs["validate"](self)
320
321 def onChange(self, sender):
322 """The textbox or list selection is changed"""
323 if isinstance(sender, ListBox):
324 AutoCompleteTextBox.onChange(self, sender)
325 self.validate()
326
327 def onClick(self, sender):
328 """The list is clicked"""
329 AutoCompleteTextBox.onClick(self, sender)
330 self.validate()
331
332 def onKeyUp(self, sender, keycode, modifiers):
333 """Listen for ENTER key stroke"""
334 AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers)
335 if keycode == KEY_ENTER:
336 self.validate()
264 337
265 338
266 class DropCell(DropWidget): 339 class DropCell(DropWidget):
267 """A cell where you can drop widgets. This class is NOT generic because of 340 """A cell where you can drop widgets. This class is NOT generic because of
268 onDrop which uses methods from ListPanel. It has been created to 341 onDrop which uses methods from ListPanel. It has been created to
269 separate the drag and drop methods from the others and add a bit of 342 separate the drag and drop methods from the others and add a bit of
270 lisibility, but it's probably not reusable for another scenario. 343 lisibility, but it's probably not reusable for another scenario.
271 """ 344 """
272 345
273 def __init__(self, host): 346 def __init__(self, drop_cbs):
274 DropWidget.__init__(self) 347 DropWidget.__init__(self)
348 self.drop_cbs = drop_cbs
275 349
276 def onDragEnter(self, event): 350 def onDragEnter(self, event):
277 self.addStyleName(self.style["dragoverPanel"]) 351 self.addStyleName(self.style["dragoverPanel"])
278 DOM.eventPreventDefault(event) 352 DOM.eventPreventDefault(event)
279 353
293 dt = event.dataTransfer 367 dt = event.dataTransfer
294 # 'text', 'text/plain', and 'Text' are equivalent. 368 # 'text', 'text/plain', and 'Text' are equivalent.
295 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed 369 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed
296 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and 370 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and
297 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report 371 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report
298 if item_type == "GROUP": 372 if item_type in self.drop_cbs.keys():
299 item = "@%s" % item 373 self.drop_cbs[item_type](self, item)
300 self.addContact(item)
301 elif item_type == "CONTACT":
302 self.addContact(item)
303 elif item_type == "CONTACT_TEXTBOX":
304 self._parent.setTargetDropCell(self)
305 pass
306 else:
307 return
308 self.removeStyleName(self.style["dragoverPanel"]) 374 self.removeStyleName(self.style["dragoverPanel"])
309 375
310 376
311 class ListPanel(FlowPanel, DropCell, FocusHandler, KeyboardHandler): 377 VALID = 1
378 INVALID = 2
379 DELETE = 3
380
381
382 class ListPanel(FlowPanel, DropCell):
312 """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel 383 """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel
313 is not fully implemented yet and can not be used with pyjamas.ui.Label.""" 384 is not fully implemented yet and can not be used with pyjamas.ui.Label."""
314 385
315 def __init__(self, parent, entry, style={}): 386 def __init__(self, parent, entry, style={}):
316 """Initialization with a button and a DragAutoCompleteTextBox.""" 387 """Initialization with a button and a DragAutoCompleteTextBox."""
317 FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) 388 FlowPanel.__init__(self, Visible=(False if entry["optional"] else True))
318 DropCell.__init__(self) 389 drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item),
390 "CONTACT": lambda panel, item: self.addContact(item),
391 "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel)
392 }
393 DropCell.__init__(self, drop_cbs)
319 self.style = style 394 self.style = style
320 self.addStyleName(self.style["keyPanel"]) 395 self.addStyleName(self.style["keyPanel"])
321 self._parent = parent 396 self._parent = parent
322 self.host = parent.host 397 self.key = entry["title"]
323 398 self._addTextBox()
324 self._last_textbox = None 399
325 self.__remove_cbs = [] 400 def _addTextBox(self, switchPrevious=False):
326 401 """Add a text box to the last position. If switchPrevious is True, simulate
327 self.__resetLastTextBox() 402 an insertion before the current last textbox by copying the text and valid state.
328 403 @return: the created textbox or the previous one if switchPrevious is True.
329 def __resetLastTextBox(self, setFocus=True): 404 """
330 """Reset the last input text box with KeyboardListener.""" 405 if hasattr(self, "_last_textbox"):
331 if self._last_textbox is None: 406 if self._last_textbox.getText() == "":
332 self._last_textbox = DragAutoCompleteTextBox() 407 return
333 self._last_textbox.addStyleName(self.style["textBox"]) 408 self._last_textbox.setRemoveButton()
334 self._last_textbox.addKeyboardListener(self)
335 self._last_textbox.addFocusListener(self)
336 else: 409 else:
337 # ensure we move it to the last position 410 switchPrevious = False
338 self.remove(self._last_textbox) 411
339 self._last_textbox.setText("") 412 def focus_cb(sender):
340 self.add(self._last_textbox) 413 if sender != self._last_textbox:
341 self._last_textbox.setFocus(setFocus) 414 # save the current value before it's being modified
342 415 self._parent.addToRemainingList(sender.getText(), ignore_key=self.key)
343 def onKeyUp(self, sender, keycode, modifiers): 416 sender.setCompletionItems(self._parent.remaining_list)
344 """This is called after DragAutoCompleteTextBox.onKeyDown,
345 so the completion is done before we reset the text box."""
346 if not isinstance(sender, DragAutoCompleteTextBox):
347 return
348 if keycode == KEY_ENTER:
349 self.onLostFocus(sender)
350 self._last_textbox.setFocus(True)
351
352 def onFocus(self, sender):
353 """A DragAutoCompleteTextBox has the focus."""
354 if not isinstance(sender, DragAutoCompleteTextBox):
355 return
356 if sender != self._last_textbox:
357 # save the current value before it's being modified
358 text = sender.getText()
359 self._focused_textbox_previous_value = text
360 self._parent.addToRemainingList(text)
361 sender.setCompletionItems(self._parent.remaining_list)
362
363 def onLostFocus(self, sender):
364 """A DragAutoCompleteTextBox has lost the focus."""
365 if not isinstance(sender, DragAutoCompleteTextBox):
366 return
367 self.changeContact(sender)
368
369 def changeContact(self, sender):
370 """Modify the value of a DragAutoCompleteTextBox."""
371 text = sender.getText()
372 if sender == self._last_textbox:
373 if text != "":
374 # a new box is added and the last textbox is reinitialized
375 self.addContact(text, setFocusToLastTextBox=False)
376 return
377 if text == "":
378 sender.remove_btn.click()
379 return
380 # text = new value needs to be removed 1. if the value is unchanged, because we
381 # added it when we took the focus, or 2. if the value is changed (obvious!)
382 self._parent.removeFromRemainingList(text)
383 if text == self._focused_textbox_previous_value:
384 return
385 sender.setVisibleLength(len(text))
386 self._parent.addToRemainingList(self._focused_textbox_previous_value)
387
388 def addContact(self, contact, resetLastTextBox=True, setFocusToLastTextBox=True):
389 """Add a contact and signal it to self._parent panel."""
390 if contact is None or contact == "":
391 return
392 textbox = DragAutoCompleteTextBox()
393 textbox.addStyleName(self.style["textBox"])
394 textbox.setText(contact)
395 self.add(textbox)
396 try:
397 textbox.setVisibleLength(len(str(contact)))
398 except:
399 #FIXME: . how come could this happen?! len(contact) is sometimes 0 but contact is not empty
400 print "len(contact) returns %d where contact == %s..." % (len(str(contact)), str(contact))
401 self._parent.removeFromRemainingList(contact)
402
403 remove_btn = Button(REMOVE_BUTTON, Visible=False)
404 remove_btn.setStyleName(self.style["removeButton"])
405 417
406 def remove_cb(sender): 418 def remove_cb(sender):
407 """Callback for the button to remove this contact.""" 419 """Callback for the button to remove this contact."""
408 self.remove(textbox) 420 self._parent.addToRemainingList(sender.getText())
409 self.remove(remove_btn)
410 self._parent.addToRemainingList(contact)
411 self._parent.setRemainingListUnsorted() 421 self._parent.setRemainingListUnsorted()
412 422 self._last_textbox.setFocus(True)
413 remove_btn.addClickListener(remove_cb) 423
414 self.__remove_cbs.append(remove_cb) 424 def drop_cb(sender, target):
415 self.add(remove_btn) 425 """Callback when the textbox is drag-n-dropped."""
416 self.__resetLastTextBox(setFocus=setFocusToLastTextBox) 426 parent = sender._parent
417 427 if target != parent and target.addContact(sender.getText()):
418 textbox.remove_btn = remove_btn 428 sender.removeOrReset()
419 textbox.addFocusListener(self) 429 else:
420 textbox.addKeyboardListener(self) 430 parent._parent.removeFromRemainingList(sender.getText())
431
432 events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb}
433 textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
434 self.add(textbox)
435 if switchPrevious:
436 textbox.setText(self._last_textbox.getText())
437 textbox.setValid(self._last_textbox.valid)
438 self._last_textbox.reset()
439 previous = self._last_textbox
440 self._last_textbox = textbox
441 return previous if switchPrevious else textbox
442
443 def _checkContact(self, contact, modify):
444 """
445 @param contact: the contact to check
446 @param modify: True if the contact is being modified
447 @return:
448 - VALID if the contact is valid
449 - INVALID if the contact is not valid but can be displayed
450 - DELETE if the contact should not be displayed at all
451 """
452 def countItemInList(list_, item):
453 """For some reason the built-in count function doesn't work..."""
454 count = 0
455 for elem in list_:
456 if elem == item:
457 count += 1
458 return count
459 if contact is None or contact == "":
460 return DELETE
461 if countItemInList(self.getContacts(), contact) > (1 if modify else 0):
462 return DELETE
463 return VALID if contact in self._parent.list else INVALID
464
465 def addContact(self, contact, sender=None):
466 """The first parameter type is checked, so it is also possible to call addContact(sender).
467 If contact is not defined, sender.getText() is used. If sender is not defined, contact will
468 be written to the last textbox and a new textbox is added afterward.
469 @param contact: unicode
470 @param sender: DragAutoCompleteTextBox instance
471 """
472 if isinstance(contact, DragAutoCompleteTextBox):
473 sender = contact
474 contact = sender.getText()
475 valid = self._checkContact(contact, sender is not None)
476 if sender is None:
477 # method has been called to modify but to add a contact
478 if valid == VALID:
479 # eventually insert before the last textbox if it's not empty
480 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
481 sender.setText(contact)
482 else:
483 sender.setValid(valid == VALID)
484 if valid != VALID:
485 if sender is not None and valid == DELETE:
486 sender.removeOrReset()
487 return False
488 if sender == self._last_textbox:
489 self._addTextBox()
490 try:
491 sender.setVisibleLength(len(contact))
492 except:
493 # IndexSizeError: Index or size is negative or greater than the allowed amount
494 print "FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))
495 self._parent.removeFromRemainingList(contact)
496 self._last_textbox.setFocus(True)
497 return True
421 498
422 def emptyContacts(self): 499 def emptyContacts(self):
423 """Empty the list of contacts.""" 500 """Empty the list of contacts."""
424 for remove_cb in self.__remove_cbs: 501 for child in self.getChildren():
425 remove_cb() 502 if hasattr(child, "remove_btn"):
426 self.__remove_cbs = [] 503 child.remove_btn.click()
427 504
428 def setContacts(self, tab): 505 def setContacts(self, tab):
429 """Set the contacts.""" 506 """Set the contacts."""
430 self.emptyContacts() 507 self.emptyContacts()
431 if isinstance(tab, set): 508 if isinstance(tab, set):
432 tab = list(tab) 509 tab = list(tab)
433 tab.sort() 510 tab.sort()
434 for contact in tab: 511 for contact in tab:
435 self.addContact(contact, resetLastTextBox=False) 512 self.addContact(contact)
436 self.__resetLastTextBox()
437 513
438 def getContacts(self): 514 def getContacts(self):
439 """Get the contacts 515 """Get the contacts
440 @return: an array of string""" 516 @return: an array of string"""
441 tab = [] 517 tab = []
444 # not to be mixed with EMPTY_SELECTION_ITEM 520 # not to be mixed with EMPTY_SELECTION_ITEM
445 if widget.getText() != "": 521 if widget.getText() != "":
446 tab.append(widget.getText()) 522 tab.append(widget.getText())
447 return tab 523 return tab
448 524
449 def getTargetDropCell(self): 525 @property
450 """Returns self or another panel where something has been dropped.""" 526 def target_drop_cell(self):
451 return self._parent.getTargetDropCell() 527 """@return: the panel where something has been dropped."""
528 return self._parent.target_drop_cell
529
530 @target_drop_cell.setter
531 def target_drop_cell(self, target_drop_cell):
532 """@param target_drop_cell: the panel where something has been dropped."""
533 self.setTargetDropCell(target_drop_cell)
534
535 def setTargetDropCell(self, target_drop_cell):
536 self._parent.target_drop_cell = target_drop_cell
452 537
453 538
454 class ContactChooserPanel(DialogBox): 539 class ContactChooserPanel(DialogBox):
455 """Display the contacts chooser dialog. This has been implemented while 540 """Display the contacts chooser dialog. This has been implemented while
456 prototyping and is currently not used. Left for an eventual later use. 541 prototyping and is currently not used. Left for an eventual later use.