Mercurial > libervia-web
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. |