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