Mercurial > libervia-web
comparison src/browser/sat_browser/list_manager.py @ 604:c22b47d63fe2 frontends_multi_profiles
browser_side: fixed DragAutoCompleteTextBox for the list manager
author | souliane <souliane@mailoo.org> |
---|---|
date | Sat, 07 Feb 2015 20:35:45 +0100 |
parents | 32dbbc941123 |
children | 6d3142b782c3 |
comparison
equal
deleted
inserted
replaced
603:462d0458e679 | 604:c22b47d63fe2 |
---|---|
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 log = getLogger(__name__) | 21 log = getLogger(__name__) |
22 from pyjamas.ui.Button import Button | 22 from pyjamas.ui.Button import Button |
23 from pyjamas.ui.ListBox import ListBox | |
24 from pyjamas.ui.FlowPanel import FlowPanel | 23 from pyjamas.ui.FlowPanel import FlowPanel |
25 from pyjamas.ui.AutoComplete import AutoCompleteTextBox | 24 from pyjamas.ui.AutoComplete import AutoCompleteTextBox |
26 from pyjamas.ui.KeyboardListener import KEY_ENTER | 25 from pyjamas.ui.KeyboardListener import KEY_ENTER |
27 from pyjamas.ui.MouseListener import MouseHandler | 26 from pyjamas.ui.DragWidget import DragWidget |
28 from pyjamas.ui.FocusListener import FocusHandler | |
29 from pyjamas.ui.DropWidget import DropWidget | |
30 from pyjamas.Timer import Timer | 27 from pyjamas.Timer import Timer |
31 from pyjamas import DOM | |
32 | 28 |
33 import base_panels | 29 import base_panels |
34 import base_widget | 30 import base_widget |
35 | 31 |
36 from sat_frontends.tools import jid | 32 from sat_frontends.tools import jid |
78 self.offsets.update(offsets) | 74 self.offsets.update(offsets) |
79 | 75 |
80 self.style = {"keyItem": "itemKey", | 76 self.style = {"keyItem": "itemKey", |
81 "popupMenuItem": "itemKey", | 77 "popupMenuItem": "itemKey", |
82 "buttonCell": "itemButtonCell", | 78 "buttonCell": "itemButtonCell", |
83 "dragoverPanel": "itemPanel-dragover", | |
84 "keyPanel": "itemPanel", | 79 "keyPanel": "itemPanel", |
85 "textBox": "itemTextBox", | 80 "textBox": "itemTextBox", |
86 "textBox-invalid": "itemTextBox-invalid", | 81 "textBox-invalid": "itemTextBox-invalid", |
87 "removeButton": "itemRemoveButton", | 82 "removeButton": "itemRemoveButton", |
88 } | 83 } |
256 the button widget and the item key. | 251 the button widget and the item key. |
257 """ | 252 """ |
258 self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]}) | 253 self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]}) |
259 | 254 |
260 | 255 |
261 class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler): | 256 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget): |
262 """A draggable AutoCompleteTextBox which is used for representing an item.""" | 257 """A draggable AutoCompleteTextBox which is used for representing an item.""" |
263 # XXX: this class is NOT generic because of the onDragEnd method which calls methods from ListPanel. It's probably not reusable for another scenario. | |
264 | 258 |
265 def __init__(self, list_panel, event_cbs, style): | 259 def __init__(self, list_panel, event_cbs, style): |
266 """ | 260 """ |
267 | 261 |
268 @param list_panel (ListPanel) | 262 @param list_panel (ListPanel) |
269 @param event_cbs (list[callable]) | 263 @param event_cbs (list[callable]) |
270 @param style (dict) | 264 @param style (dict) |
271 """ | 265 """ |
272 AutoCompleteTextBox.__init__(self) | 266 AutoCompleteTextBox.__init__(self) |
273 base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type | 267 DragWidget.__init__(self) |
274 self.list_panel = list_panel | 268 self.list_panel = list_panel |
275 self.event_cbs = event_cbs | 269 self.event_cbs = event_cbs |
276 self.style = style | 270 self.style = style |
271 self.addStyleName(style["textBox"]) | |
272 self.reset() | |
273 | |
274 # Parent classes already init self as an handler for these events | |
277 self.addMouseListener(self) | 275 self.addMouseListener(self) |
278 self.addFocusListener(self) | 276 self.addFocusListener(self) |
279 self.addChangeListener(self) | 277 self.addChangeListener(self) |
280 self.addStyleName(style["textBox"]) | 278 |
281 self.reset() | 279 def onDragStart(self, event): |
280 """The user starts dragging the text box.""" | |
281 self.list_panel.manager.target_drop_cell = None | |
282 self.setSelectionRange(len(self.getText()), 0) | |
283 | |
284 dt = event.dataTransfer | |
285 dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX")) | |
286 dt.setDragImage(self.getElement(), 15, 15) | |
287 | |
288 def onDragEnd(self, event): | |
289 """The user dropped the text box.""" | |
290 target = self.list_panel.manager.target_drop_cell # parent or another ListPanel | |
291 if self.getText() == "" or target is None: | |
292 return | |
293 self.event_cbs["drop"](self, target) | |
294 | |
295 def onClick(self, sender): | |
296 """The choices list is clicked""" | |
297 assert sender == self.choices | |
298 AutoCompleteTextBox.onClick(self, sender) | |
299 self.validate() | |
300 | |
301 def onChange(self, sender): | |
302 """The list selection or the text has been changed""" | |
303 assert sender == self.choices or sender == self | |
304 if sender == self.choices: | |
305 AutoCompleteTextBox.onChange(self, sender) | |
306 self.validate() | |
307 | |
308 def onKeyUp(self, sender, keycode, modifiers): | |
309 """Listen for key stroke""" | |
310 assert sender == self | |
311 AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) | |
312 if keycode == KEY_ENTER: | |
313 self.validate() | |
314 | |
315 def onMouseMove(self, sender): | |
316 """Mouse enters the area of a DragAutoCompleteTextBox.""" | |
317 assert sender == self | |
318 if hasattr(sender, "remove_btn"): | |
319 sender.remove_btn.setVisible(True) | |
320 | |
321 def onMouseLeave(self, sender): | |
322 """Mouse leaves the area of a DragAutoCompleteTextBox.""" | |
323 assert sender == self | |
324 if hasattr(sender, "remove_btn"): | |
325 Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) | |
326 | |
327 def onFocus(self, sender): | |
328 """The DragAutoCompleteTextBox has the focus.""" | |
329 assert sender == self | |
330 # FIXME: this raises runtime JS error "Permission denied to access property..." when you drag the object | |
331 #sender.setSelectionRange(0, len(sender.getText())) | |
332 sender.event_cbs["focus"](sender) | |
282 | 333 |
283 def reset(self): | 334 def reset(self): |
335 """Reset the text box""" | |
284 self.setText("") | 336 self.setText("") |
285 self.setValid() | 337 self.setValid() |
286 | 338 |
287 def setValid(self, valid=True): | 339 def setValid(self, valid=True): |
340 """Change the style according to the text validity.""" | |
288 if self.getText() == "": | 341 if self.getText() == "": |
289 valid = True | 342 valid = True |
290 if valid: | 343 if valid: |
291 self.removeStyleName(self.style["textBox-invalid"]) | 344 self.removeStyleName(self.style["textBox-invalid"]) |
292 else: | 345 else: |
293 self.addStyleName(self.style["textBox-invalid"]) | 346 self.addStyleName(self.style["textBox-invalid"]) |
294 self.valid = valid | 347 self.valid = valid |
295 | 348 |
296 def onDragStart(self, event): | 349 def validate(self): |
297 self._text = self.getText() | 350 """Check if the text is valid, update the style.""" |
298 base_widget.DragLabel.onDragStart(self, event) | |
299 self.list_panel.manager.target_drop_cell = None | |
300 self.setSelectionRange(len(self.getText()), 0) | 351 self.setSelectionRange(len(self.getText()), 0) |
301 | 352 self.event_cbs["validate"](self) |
302 def onDragEnd(self, event): | |
303 target = self.list_panel.manager.target_drop_cell # parent or another ListPanel | |
304 if self.getText() == "" or target is None: | |
305 return | |
306 self.event_cbs["drop"](self, target) | |
307 | 353 |
308 def setRemoveButton(self): | 354 def setRemoveButton(self): |
355 """Add the remove button after the text box.""" | |
309 | 356 |
310 def remove_cb(sender): | 357 def remove_cb(sender): |
311 """Callback for the button to remove this item.""" | 358 """Callback for the button to remove this item.""" |
312 self.list_panel.remove(self) | 359 self.list_panel.remove(self) |
313 self.list_panel.remove(self.remove_btn) | 360 self.list_panel.remove(self.remove_btn) |
316 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) | 363 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) |
317 self.remove_btn.setStyleName(self.style["removeButton"]) | 364 self.remove_btn.setStyleName(self.style["removeButton"]) |
318 self.list_panel.add(self.remove_btn) | 365 self.list_panel.add(self.remove_btn) |
319 | 366 |
320 def removeOrReset(self): | 367 def removeOrReset(self): |
368 """Remove the text box if the remove button exists, or reset the text box.""" | |
321 if hasattr(self, "remove_btn"): | 369 if hasattr(self, "remove_btn"): |
322 self.remove_btn.click() | 370 self.remove_btn.click() |
323 else: | 371 else: |
324 self.reset() | 372 self.reset() |
325 | 373 |
326 def onMouseMove(self, sender): | |
327 """Mouse enters the area of a DragAutoCompleteTextBox.""" | |
328 if hasattr(sender, "remove_btn"): | |
329 sender.remove_btn.setVisible(True) | |
330 | |
331 def onMouseLeave(self, sender): | |
332 """Mouse leaves the area of a DragAutoCompleteTextBox.""" | |
333 if hasattr(sender, "remove_btn"): | |
334 Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) | |
335 | |
336 def onFocus(self, sender): | |
337 sender.setSelectionRange(0, len(self.getText())) | |
338 self.event_cbs["focus"](sender) | |
339 | |
340 def validate(self): | |
341 self.setSelectionRange(len(self.getText()), 0) | |
342 self.event_cbs["validate"](self) | |
343 | |
344 def onChange(self, sender): | |
345 """The textbox or list selection is changed""" | |
346 if isinstance(sender, ListBox): | |
347 AutoCompleteTextBox.onChange(self, sender) | |
348 self.validate() | |
349 | |
350 def onClick(self, sender): | |
351 """The list is clicked""" | |
352 AutoCompleteTextBox.onClick(self, sender) | |
353 self.validate() | |
354 | |
355 def onKeyUp(self, sender, keycode, modifiers): | |
356 """Listen for ENTER key stroke""" | |
357 AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) | |
358 if keycode == KEY_ENTER: | |
359 self.validate() | |
360 | |
361 | |
362 class DropCell(DropWidget): | |
363 """A cell where you can drop widgets. This class is NOT generic because of | |
364 onDrop which uses methods from ListPanel. It has been created to | |
365 separate the drag and drop methods from the others and add a bit of | |
366 readability, but it's probably not reusable for another scenario. | |
367 """ | |
368 | |
369 def __init__(self, drop_cbs): | |
370 """ | |
371 | |
372 @param drop_cbs (list[callable]) | |
373 """ | |
374 DropWidget.__init__(self) | |
375 self.drop_cbs = drop_cbs | |
376 | |
377 def onDragEnter(self, event): | |
378 self.addStyleName(self.style["dragoverPanel"]) | |
379 DOM.eventPreventDefault(event) | |
380 | |
381 def onDragLeave(self, event): | |
382 if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\ | |
383 or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\ | |
384 or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: | |
385 # We check that we are inside widget's box, and we don't remove the style in this case because | |
386 # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed | |
387 self.removeStyleName(self.style["dragoverPanel"]) | |
388 | |
389 def onDragOver(self, event): | |
390 DOM.eventPreventDefault(event) | |
391 | |
392 def onDrop(self, event): | |
393 DOM.eventPreventDefault(event) | |
394 dt = event.dataTransfer | |
395 # 'text', 'text/plain', and 'Text' are equivalent. | |
396 item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed | |
397 if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and | |
398 item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report | |
399 if item_type in self.drop_cbs.keys(): | |
400 self.drop_cbs[item_type](self, item) | |
401 self.removeStyleName(self.style["dragoverPanel"]) | |
402 | |
403 | 374 |
404 VALID = 1 | 375 VALID = 1 |
405 INVALID = 2 | 376 INVALID = 2 |
406 DELETE = 3 | 377 DELETE = 3 |
407 | 378 |
408 | 379 |
409 class ListPanel(FlowPanel, DropCell): | 380 class ListPanel(FlowPanel, base_widget.DropCell): |
410 """Panel used for listing items sharing the same key. The key is showed as | 381 """Panel used for listing items sharing the same key. The key is showed as |
411 a Button to which you can bind a popup menu and the items are represented | 382 a Button to which you can bind a popup menu and the items are represented |
412 with a sequence of DragAutoCompleteTextBoxeditable.""" | 383 with a sequence of DragAutoCompleteTextBox.""" |
413 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label | 384 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label |
414 | 385 |
415 def __init__(self, manager, data, style={}): | 386 def __init__(self, manager, data, style={}): |
416 """Initialization with a button and a DragAutoCompleteTextBox. | 387 """Initialization with a button and a DragAutoCompleteTextBox. |
417 | 388 |
419 @param data (dict{unicode: unicode}) | 390 @param data (dict{unicode: unicode}) |
420 @param style (dict{unicode: unicode}) | 391 @param style (dict{unicode: unicode}) |
421 """ | 392 """ |
422 FlowPanel.__init__(self, Visible=(False if data["optional"] else True)) | 393 FlowPanel.__init__(self, Visible=(False if data["optional"] else True)) |
423 | 394 |
424 def setTargetDropCell(panel, item): | 395 def setTargetDropCell(host, item): |
425 self.manager.target_drop_cell = panel | 396 self.manager.target_drop_cell = self |
426 | 397 |
427 # FIXME: dirty magic strings '@' and '@@' | 398 # FIXME: dirty magic strings '@' and '@@' |
428 drop_cbs = {"GROUP": lambda panel, item: self.addItem("@%s" % item), | 399 drop_cbs = {"GROUP": lambda host, item: self.addItem("@%s" % item), |
429 "CONTACT": lambda panel, item: self.addItem(tryJID(item)), | 400 "CONTACT": lambda host, item: self.addItem(tryJID(item)), |
430 "CONTACT_TITLE": lambda panel, item: self.addItem('@@'), | 401 "CONTACT_TITLE": lambda host, item: self.addItem('@@'), |
431 "CONTACT_TEXTBOX": setTargetDropCell | 402 "CONTACT_TEXTBOX": setTargetDropCell |
432 } | 403 } |
433 DropCell.__init__(self, drop_cbs) | 404 base_widget.DropCell.__init__(self, None) |
405 self.drop_keys = drop_cbs | |
434 self.style = style | 406 self.style = style |
435 self.addStyleName(self.style["keyPanel"]) | 407 self.addStyleName(self.style["keyPanel"]) |
436 self.manager = manager | 408 self.manager = manager |
437 self.key = data["title"] | 409 self.key = data["title"] |
438 self._addTextBox() | 410 self._addTextBox() |
411 | |
412 def onDrop(self, event): | |
413 try: | |
414 base_widget.DropCell.onDrop(self, event) | |
415 except base_widget.NoLiberviaWidgetException: | |
416 pass | |
439 | 417 |
440 def _addTextBox(self, switchPrevious=False): | 418 def _addTextBox(self, switchPrevious=False): |
441 """Add an empty text box to the last position. | 419 """Add an empty text box to the last position. |
442 | 420 |
443 @param switchPrevious (bool): if True, simulate an insertion before the | 421 @param switchPrevious (bool): if True, simulate an insertion before the |
506 if count(self.getItems(), item) > (1 if modify else 0): | 484 if count(self.getItems(), item) > (1 if modify else 0): |
507 return DELETE | 485 return DELETE |
508 return VALID if item in self.manager.items else INVALID | 486 return VALID if item in self.manager.items else INVALID |
509 | 487 |
510 def addItem(self, item, sender=None): | 488 def addItem(self, item, sender=None): |
511 """ | 489 """Try to add an item. It will be added if it's a valid one. |
512 | 490 |
513 @param item (object): item to be added | 491 @param item (object): item to be added |
514 @param (DragAutoCompleteTextBox): widget triggering the event | 492 @param (DragAutoCompleteTextBox): widget triggering the event |
515 @param sender: if True, the item will be "written" to the last textbox | 493 @param sender: if True, the item will be "written" to the last textbox |
516 and a new text box will be added afterward. | 494 and a new text box will be added afterward. |
495 @return: True if the item has been added. | |
517 """ | 496 """ |
518 valid = self._checkItem(item, sender is not None) | 497 valid = self._checkItem(item, sender is not None) |
519 item_s = unicode(item) | 498 item_s = unicode(item) |
520 if sender is None: | 499 if sender is None: |
521 # method has been called not to modify but to add an item | 500 # method has been called not to modify but to add an item |