comparison src/browser/sat_browser/list_manager.py @ 600:32dbbc941123 frontends_multi_profiles

browser_side: fixes the contact group manager
author souliane <souliane@mailoo.org>
date Fri, 06 Feb 2015 17:53:01 +0100
parents 97c72fe4a5f2
children c22b47d63fe2
comparison
equal deleted inserted replaced
599:a6b9809b9a68 600:32dbbc941123
17 # You should have received a copy of the GNU Affero General Public License 17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.log import getLogger 20 from sat.core.log import getLogger
21 log = getLogger(__name__) 21 log = getLogger(__name__)
22 from pyjamas.ui.Grid import Grid
23 from pyjamas.ui.Button import Button 22 from pyjamas.ui.Button import Button
24 from pyjamas.ui.ListBox import ListBox 23 from pyjamas.ui.ListBox import ListBox
25 from pyjamas.ui.FlowPanel import FlowPanel 24 from pyjamas.ui.FlowPanel import FlowPanel
26 from pyjamas.ui.AutoComplete import AutoCompleteTextBox 25 from pyjamas.ui.AutoComplete import AutoCompleteTextBox
27 from pyjamas.ui.Label import Label
28 from pyjamas.ui.HorizontalPanel import HorizontalPanel
29 from pyjamas.ui.VerticalPanel import VerticalPanel
30 from pyjamas.ui.DialogBox import DialogBox
31 from pyjamas.ui.KeyboardListener import KEY_ENTER 26 from pyjamas.ui.KeyboardListener import KEY_ENTER
32 from pyjamas.ui.MouseListener import MouseHandler 27 from pyjamas.ui.MouseListener import MouseHandler
33 from pyjamas.ui.FocusListener import FocusHandler 28 from pyjamas.ui.FocusListener import FocusHandler
34 from pyjamas.ui.DropWidget import DropWidget 29 from pyjamas.ui.DropWidget import DropWidget
35 from pyjamas.Timer import Timer 30 from pyjamas.Timer import Timer
36 from pyjamas import DOM 31 from pyjamas import DOM
37 32
38 import base_panels 33 import base_panels
39 import base_widget 34 import base_widget
40 35
36 from sat_frontends.tools import jid
37
38
39 unicode = str # FIXME: pyjamas workaround
40
41 # HTML content for the removal button (image or text) 41 # HTML content for the removal button (image or text)
42 REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' 42 REMOVE_BUTTON = '<span class="itemRemoveIcon">x</span>'
43 43
44 # Item to be considered for an empty list box selection. 44
45 # Could be whatever which doesn't look like a JID or a group name. 45 # FIXME: dirty method and magic string to fix ASAP
46 EMPTY_SELECTION_ITEM = "" 46 def tryJID(obj):
47 47 return jid.JID(obj) if (isinstance(obj, unicode) and not obj.startswith('@')) else obj
48 48
49 class ListManager(): 49
50 """A manager for sub-panels to assign elements to lists.""" 50 class ListManager(object):
51 51 """A base class to manage one or several lists of items."""
52 def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): 52
53 """ 53 def __init__(self, container, keys=None, items=None, offsets=None, style=None):
54 @param parent: FlexTable parent widget for the manager 54 """
55 @param keys_dict: dict with the contact keys mapped to data 55 @param container (FlexTable): FlexTable parent widget
56 @param contact_list: list of string (the contact JID userhosts) 56 @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items
57 @param offsets: dict to set widget positions offset within parent 57 keys to their display config data.
58 - "x_first": the x offset for the first widget's row on the grid 58 @param items (list): list of items
59 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined 59 @param offsets (dict): define widgets positions offsets within container:
60 - "y": the y offset for all widgets columns on the grid 60 - "x_first": the x offset for the first widget's row on the grid
61 """ 61 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined
62 self._parent = parent 62 - "y": the y offset for all widgets columns on the grid
63 if isinstance(keys_dict, set) or isinstance(keys_dict, list): 63 @param style (dict): define CSS styles
64 tmp = {} 64 """
65 for key in keys_dict: 65 self.container = container
66 tmp[key] = {} 66 self.keys = {} if keys is None else keys
67 keys_dict = tmp 67 self.items = [] if items is None else items
68 self.__keys_dict = keys_dict 68 self.items.sort()
69 if isinstance(contact_list, set): 69
70 contact_list = list(contact_list) 70 # store the list of items that are not assigned yet
71 self.__list = contact_list 71 self.items_remaining = [item for item in self.items]
72 self.__list.sort() 72 self.items_remaining_sorted = True
73 # store the list of contacts that are not assigned yet
74 self.__remaining_list = []
75 self.__remaining_list.extend(self.__list)
76 # mark a change to sort the list before it's used
77 self.__remaining_list_sorted = True
78 73
79 self.offsets = {"x_first": 0, "x": 0, "y": 0} 74 self.offsets = {"x_first": 0, "x": 0, "y": 0}
80 if "x" in offsets and not "x_first" in offsets: 75 if offsets is not None:
81 offsets["x_first"] = offsets["x"] 76 if "x" in offsets and "x_first" not in offsets:
82 self.offsets.update(offsets) 77 offsets["x_first"] = offsets["x"]
83 78 self.offsets.update(offsets)
84 self.style = { 79
85 "keyItem": "recipientTypeItem", 80 self.style = {"keyItem": "itemKey",
86 "popupMenuItem": "recipientTypeItem", 81 "popupMenuItem": "itemKey",
87 "buttonCell": "recipientButtonCell", 82 "buttonCell": "itemButtonCell",
88 "dragoverPanel": "dragover-recipientPanel", 83 "dragoverPanel": "itemPanel-dragover",
89 "keyPanel": "recipientPanel", 84 "keyPanel": "itemPanel",
90 "textBox": "recipientTextBox", 85 "textBox": "itemTextBox",
91 "textBox-invalid": "recipientTextBox-invalid", 86 "textBox-invalid": "itemTextBox-invalid",
92 "removeButton": "recipientRemoveButton", 87 "removeButton": "itemRemoveButton",
93 } 88 }
94 self.style.update(style) 89 if style is not None:
90 self.style.update(style)
95 91
96 def createWidgets(self, title_format="%s"): 92 def createWidgets(self, title_format="%s"):
97 """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" 93 """Fill the container widget with one ListPanel per item key (some may be
98 self.__children = {} 94 hidden during the initialization).
99 for key in self.__keys_dict: 95
100 self.addContactKey(key, title_format=title_format) 96 @param title_format (unicode): format string for the title
101 97 """
102 def addContactKey(self, key, dict_={}, title_format="%s"): 98 self.children = {}
103 if key not in self.__keys_dict: 99 for key in self.keys:
104 self.__keys_dict[key] = dict_ 100 self.addItemKey(key, title_format=title_format)
105 # copy the key to its associated sub-map 101
106 self.__keys_dict[key]["title"] = key 102 def addItemKey(self, key, data=None, title_format="%s"):
107 self._addChild(self.__keys_dict[key], title_format) 103 """Add to the container a Button and ListPanel for a new item key.
108 104
109 def removeContactKey(self, key): 105 @param key (unicode): item key
110 """Remove a list panel and all its associated data.""" 106 @param data (dict{unicode: unicode}): config data
111 contacts = self.__children[key]["panel"].getContacts() 107 """
112 (y, x) = self._parent.getIndex(self.__children[key]["button"]) 108 key_data = self.keys.setdefault(key, {})
113 self._parent.removeRow(y) 109 if data is not None:
114 del self.__children[key] 110 key_data.update(data)
115 del self.__keys_dict[key] 111 key_data["title"] = key # copy the key to its associated sub-map
116 self.addToRemainingList(contacts) 112
117 113 button = Button(title_format % key)
118 def _addChild(self, entry, title_format):
119 """Add a button and FlowPanel for the corresponding map entry."""
120 button = Button(title_format % entry["title"])
121 button.setStyleName(self.style["keyItem"]) 114 button.setStyleName(self.style["keyItem"])
122 if hasattr(entry, "desc"): 115 if hasattr(key_data, "desc"):
123 button.setTitle(entry["desc"]) 116 button.setTitle(key_data["desc"])
124 if not "optional" in entry: 117 if "optional" not in key_data:
125 entry["optional"] = False 118 key_data["optional"] = False
126 button.setVisible(not entry["optional"]) 119 button.setVisible(not key_data["optional"])
127 y = len(self.__children) + self.offsets["y"] 120 y = len(self.children) + self.offsets["y"]
128 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] 121 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"]
129 122
130 self._parent.insertRow(y) 123 self.container.insertRow(y)
131 self._parent.setWidget(y, x, button) 124 self.container.setWidget(y, x, button)
132 self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) 125 self.container.getCellFormatter().setStyleName(y, x, self.style["buttonCell"])
133 126
134 _child = ListPanel(self, entry, self.style) 127 _child = ListPanel(self, key_data, self.style)
135 self._parent.setWidget(y, x + 1, _child) 128 self.container.setWidget(y, x + 1, _child)
136 129
137 self.__children[entry["title"]] = {} 130 self.children[key] = {}
138 self.__children[entry["title"]]["button"] = button 131 self.children[key]["button"] = button
139 self.__children[entry["title"]]["panel"] = _child 132 self.children[key]["panel"] = _child
140 133
141 if hasattr(self, "popup_menu"): 134 if hasattr(self, "popup_menu"):
142 # this is done if self.registerPopupMenuPanel has been called yet 135 # self.registerPopupMenuPanel has been called yet
143 self.popup_menu.registerClickSender(button) 136 self.popup_menu.registerClickSender(button)
144 137
145 def _refresh(self, visible=True): 138 def removeItemKey(self, key):
146 """Set visible the sub-panels that are non optional or non empty, hide the rest.""" 139 """Remove from the container a ListPanel representing an item key, and all
147 for key in self.__children: 140 its associated data.
148 self.setContactPanelVisible(key, False) 141
149 if not visible: 142 @param key (unicode): item key
143 """
144 items = self.children[key]["panel"].getItems()
145 (y, x) = self.container.getIndex(self.children[key]["button"])
146 self.container.removeRow(y)
147 del self.children[key]
148 del self.keys[key]
149 self.addToRemainingList(items)
150
151 def refresh(self, hide_everything=False):
152 """Set visible the sub-panels that are non optional or non empty, hide
153 the rest. Setting the attribute "hide_everything" to True you can also
154 hide everything.
155
156 @param hide_everything (boolean): set to True to hide everything
157 """
158 for key in self.children:
159 self.setItemPanelVisible(key, False)
160 if hide_everything:
150 return 161 return
151 _map = self.getContacts() 162 for key, items in self.getItemsByKey().iteritems():
152 for key in _map: 163 if len(items) > 0 or not self.keys[key]["optional"]:
153 if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: 164 self.setItemPanelVisible(key, True)
154 self.setContactPanelVisible(key, True)
155 165
156 def setVisible(self, visible): 166 def setVisible(self, visible):
157 self._refresh(visible) 167 self.refresh(not visible)
158 168
159 def setContactPanelVisible(self, key, visible=True, sender=None): 169 def setItemPanelVisible(self, key, visible=True, sender=None):
160 """Do not remove the "sender" param as it is needed for the context menu.""" 170 """Set the item key's widgets visibility.
161 self.__children[key]["button"].setVisible(visible) 171
162 self.__children[key]["panel"].setVisible(visible) 172 @param key (unicode): item key
173 @param visible (bool): set to True to display the widgets
174 @param sender
175 """
176 self.children[key]["button"].setVisible(visible)
177 self.children[key]["panel"].setVisible(visible)
163 178
164 @property 179 @property
165 def list(self): 180 def items_remaining(self):
166 """Return the full list of potential contacts.""" 181 """Return the unused items."""
167 return self.__list 182 if not self.items_remaining_sorted:
168 183 self.items_remaining.sort()
169 @property 184 self.items_remaining_sorted = True
170 def keys(self): 185 return self.items_remaining
171 return self.__keys_dict.keys()
172
173 @property
174 def keys_dict(self):
175 return self.__keys_dict
176
177 @property
178 def remaining_list(self):
179 """Return the contacts that have not been selected yet."""
180 if not self.__remaining_list_sorted:
181 self.__remaining_list_sorted = True
182 self.__remaining_list.sort()
183 return self.__remaining_list
184 186
185 def setRemainingListUnsorted(self): 187 def setRemainingListUnsorted(self):
186 """Mark a change (deletion) so the list will be sorted before it's used.""" 188 """Mark the list of unused items as being unsorted."""
187 self.__remaining_list_sorted = False 189 self.items_remaining_sorted = False
188 190
189 def removeFromRemainingList(self, contacts): 191 def removeFromRemainingList(self, items):
190 """Remove contacts after they have been added to a sub-panel.""" 192 """Remove some items from the list of unused items.
191 if not isinstance(contacts, list): 193
192 contacts = [contacts] 194 @param items (list): items to be removed
193 for contact_ in contacts: 195 """
194 if contact_ in self.__remaining_list: 196 for item in items:
195 self.__remaining_list.remove(contact_) 197 if item in self.items_remaining:
196 198 self.items_remaining.remove(item)
197 def addToRemainingList(self, contacts, ignore_key=None): 199
198 """Add contacts after they have been removed from a sub-panel.""" 200 def addToRemainingList(self, items, ignore_key=None):
199 if not isinstance(contacts, list): 201 """Add some items to the list of unused items. Check first if the
200 contacts = [contacts] 202 items are really not used in any ListPanel.
201 assigned_contacts = set() 203
202 assigned_map = self.getContacts() 204 @param items (list): items to be removed
203 for key_ in assigned_map.keys(): 205 @param ignore_key (unicode): item key to be ignored while checking
204 if ignore_key is not None and key_ == ignore_key: 206 """
207 items_assigned = set()
208 for key, current_items in self.getItemsByKey().iteritems():
209 if ignore_key is not None and key == ignore_key:
205 continue 210 continue
206 assigned_contacts.update(assigned_map[key_]) 211 items_assigned.update(current_items)
207 for contact_ in contacts: 212 for item in items:
208 if contact_ not in self.__list or contact_ in self.__remaining_list: 213 if item not in self.items or item in self.items_remaining or item in items_assigned:
209 continue 214 continue
210 if contact_ in assigned_contacts: 215 self.items_remaining.append(item)
211 continue # the contact is assigned somewhere else
212 self.__remaining_list.append(contact_)
213 self.setRemainingListUnsorted() 216 self.setRemainingListUnsorted()
214 217
215 def setContacts(self, _map={}): 218 def resetItems(self, data={}):
216 """Set the contacts for each contact key.""" 219 """Repopulate all the lists (one per item key) with the given items.
217 for key in self.__keys_dict: 220
218 if key in _map: 221 @param data (dict{unicode: list}): dict binding items keys to items.
219 self.__children[key]["panel"].setContacts(_map[key]) 222 """
223 for key in self.keys:
224 if key in data:
225 self.children[key]["panel"].resetItems(data[key])
220 else: 226 else:
221 self.__children[key]["panel"].setContacts([]) 227 self.children[key]["panel"].resetItems([])
222 self._refresh() 228 self.refresh()
223 229
224 def getContacts(self): 230 def getItemsByKey(self):
225 """Get the contacts for all the lists. 231 """Get all the items by key.
226 @return: a mapping between keys and contact lists.""" 232
227 _map = {} 233 @return: dict{unicode: set}
228 for key in self.__children: 234 """
229 _map[key] = self.__children[key]["panel"].getContacts() 235 return {key: self.children[key]["panel"].getItems() for key in self.children}
230 return _map 236
231 237 def getKeysByItem(self):
232 @property 238 """Get all the keys by item.
233 def target_drop_cell(self): 239
234 """@return: the panel where something has been dropped.""" 240 @return: dict{object: set(unicode)}
235 return self._target_drop_cell 241 """
236 242 result = {}
237 def setTargetDropCell(self, target_drop_cell): 243 for key in self.children:
238 """@param: target_drop_cell: the panel where something has been dropped.""" 244 for item in self.children[key]["panel"].getItems():
239 self._target_drop_cell = target_drop_cell 245 result.setdefault(item, set()).add(key)
246 return result
240 247
241 def registerPopupMenuPanel(self, entries, hide, callback): 248 def registerPopupMenuPanel(self, entries, hide, callback):
242 "Register a popup menu panel that will be bound to all contact keys elements." 249 """Register a popup menu panel for the item keys buttons.
243 self.popup_menu = base_panels.PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) 250
251 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries
252 @param hide (callable): method to call in order to know if a menu item
253 should be hidden from the menu. Takes in the button widget and the
254 item key and returns a boolean.
255 @param callback (callable): common callback for all menu items, takes in
256 the button widget and the item key.
257 """
258 self.popup_menu = base_panels.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]})
244 259
245 260
246 class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler): 261 class DragAutoCompleteTextBox(AutoCompleteTextBox, base_widget.DragLabel, MouseHandler, FocusHandler):
247 """A draggable AutoCompleteTextBox which is used for representing a contact. 262 """A draggable AutoCompleteTextBox which is used for representing an item."""
248 This class is NOT generic because of the onDragEnd method which call methods 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.
249 from ListPanel. It's probably not reusable for another scenario. 264
250 """ 265 def __init__(self, list_panel, event_cbs, style):
251 266 """
252 def __init__(self, parent, event_cbs, style): 267
268 @param list_panel (ListPanel)
269 @param event_cbs (list[callable])
270 @param style (dict)
271 """
253 AutoCompleteTextBox.__init__(self) 272 AutoCompleteTextBox.__init__(self)
254 base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type 273 base_widget.DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type
255 self._parent = parent 274 self.list_panel = list_panel
256 self.event_cbs = event_cbs 275 self.event_cbs = event_cbs
257 self.style = style 276 self.style = style
258 self.addMouseListener(self) 277 self.addMouseListener(self)
259 self.addFocusListener(self) 278 self.addFocusListener(self)
260 self.addChangeListener(self) 279 self.addChangeListener(self)
275 self.valid = valid 294 self.valid = valid
276 295
277 def onDragStart(self, event): 296 def onDragStart(self, event):
278 self._text = self.getText() 297 self._text = self.getText()
279 base_widget.DragLabel.onDragStart(self, event) 298 base_widget.DragLabel.onDragStart(self, event)
280 self._parent.setTargetDropCell(None) 299 self.list_panel.manager.target_drop_cell = None
281 self.setSelectionRange(len(self.getText()), 0) 300 self.setSelectionRange(len(self.getText()), 0)
282 301
283 def onDragEnd(self, event): 302 def onDragEnd(self, event):
284 target = self._parent.target_drop_cell # parent or another ListPanel 303 target = self.list_panel.manager.target_drop_cell # parent or another ListPanel
285 if self.getText() == "" or target is None: 304 if self.getText() == "" or target is None:
286 return 305 return
287 self.event_cbs["drop"](self, target) 306 self.event_cbs["drop"](self, target)
288 307
289 def setRemoveButton(self): 308 def setRemoveButton(self):
290 309
291 def remove_cb(sender): 310 def remove_cb(sender):
292 """Callback for the button to remove this contact.""" 311 """Callback for the button to remove this item."""
293 self._parent.remove(self) 312 self.list_panel.remove(self)
294 self._parent.remove(self.remove_btn) 313 self.list_panel.remove(self.remove_btn)
295 self.event_cbs["remove"](self) 314 self.event_cbs["remove"](self)
296 315
297 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) 316 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False)
298 self.remove_btn.setStyleName(self.style["removeButton"]) 317 self.remove_btn.setStyleName(self.style["removeButton"])
299 self._parent.add(self.remove_btn) 318 self.list_panel.add(self.remove_btn)
300 319
301 def removeOrReset(self): 320 def removeOrReset(self):
302 if hasattr(self, "remove_btn"): 321 if hasattr(self, "remove_btn"):
303 self.remove_btn.click() 322 self.remove_btn.click()
304 else: 323 else:
342 361
343 class DropCell(DropWidget): 362 class DropCell(DropWidget):
344 """A cell where you can drop widgets. This class is NOT generic because of 363 """A cell where you can drop widgets. This class is NOT generic because of
345 onDrop which uses methods from ListPanel. It has been created to 364 onDrop which uses methods from ListPanel. It has been created to
346 separate the drag and drop methods from the others and add a bit of 365 separate the drag and drop methods from the others and add a bit of
347 lisibility, but it's probably not reusable for another scenario. 366 readability, but it's probably not reusable for another scenario.
348 """ 367 """
349 368
350 def __init__(self, drop_cbs): 369 def __init__(self, drop_cbs):
370 """
371
372 @param drop_cbs (list[callable])
373 """
351 DropWidget.__init__(self) 374 DropWidget.__init__(self)
352 self.drop_cbs = drop_cbs 375 self.drop_cbs = drop_cbs
353 376
354 def onDragEnter(self, event): 377 def onDragEnter(self, event):
355 self.addStyleName(self.style["dragoverPanel"]) 378 self.addStyleName(self.style["dragoverPanel"])
382 INVALID = 2 405 INVALID = 2
383 DELETE = 3 406 DELETE = 3
384 407
385 408
386 class ListPanel(FlowPanel, DropCell): 409 class ListPanel(FlowPanel, DropCell):
387 """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel 410 """Panel used for listing items sharing the same key. The key is showed as
388 is not fully implemented yet and can not be used with pyjamas.ui.Label.""" 411 a Button to which you can bind a popup menu and the items are represented
389 412 with a sequence of DragAutoCompleteTextBoxeditable."""
390 def __init__(self, parent, entry, style={}): 413 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label
391 """Initialization with a button and a DragAutoCompleteTextBox.""" 414
392 FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) 415 def __init__(self, manager, data, style={}):
393 drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), 416 """Initialization with a button and a DragAutoCompleteTextBox.
394 "CONTACT": lambda panel, item: self.addContact(item), 417
395 "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), 418 @param manager (ListManager)
396 "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) 419 @param data (dict{unicode: unicode})
420 @param style (dict{unicode: unicode})
421 """
422 FlowPanel.__init__(self, Visible=(False if data["optional"] else True))
423
424 def setTargetDropCell(panel, item):
425 self.manager.target_drop_cell = panel
426
427 # FIXME: dirty magic strings '@' and '@@'
428 drop_cbs = {"GROUP": lambda panel, item: self.addItem("@%s" % item),
429 "CONTACT": lambda panel, item: self.addItem(tryJID(item)),
430 "CONTACT_TITLE": lambda panel, item: self.addItem('@@'),
431 "CONTACT_TEXTBOX": setTargetDropCell
397 } 432 }
398 DropCell.__init__(self, drop_cbs) 433 DropCell.__init__(self, drop_cbs)
399 self.style = style 434 self.style = style
400 self.addStyleName(self.style["keyPanel"]) 435 self.addStyleName(self.style["keyPanel"])
401 self._parent = parent 436 self.manager = manager
402 self.key = entry["title"] 437 self.key = data["title"]
403 self._addTextBox() 438 self._addTextBox()
404 439
405 def _addTextBox(self, switchPrevious=False): 440 def _addTextBox(self, switchPrevious=False):
406 """Add a text box to the last position. If switchPrevious is True, simulate 441 """Add an empty text box to the last position.
407 an insertion before the current last textbox by copying the text and valid state. 442
408 @return: the created textbox or the previous one if switchPrevious is True. 443 @param switchPrevious (bool): if True, simulate an insertion before the
444 current last textbox by switching the texts and valid states
445 @return: an DragAutoCompleteTextBox, the created text box or the
446 previous one if switchPrevious is True.
409 """ 447 """
410 if hasattr(self, "_last_textbox"): 448 if hasattr(self, "_last_textbox"):
411 if self._last_textbox.getText() == "": 449 if self._last_textbox.getText() == "":
412 return 450 return
413 self._last_textbox.setRemoveButton() 451 self._last_textbox.setRemoveButton()
415 switchPrevious = False 453 switchPrevious = False
416 454
417 def focus_cb(sender): 455 def focus_cb(sender):
418 if sender != self._last_textbox: 456 if sender != self._last_textbox:
419 # save the current value before it's being modified 457 # save the current value before it's being modified
420 self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) 458 self.manager.addToRemainingList([tryJID(sender.getText())], ignore_key=self.key)
421 sender.setCompletionItems(self._parent.remaining_list) 459
460 items = [unicode(item) for item in self.manager.items_remaining]
461 sender.setCompletionItems(items)
462
463 def add_cb(sender):
464 self.addItem(tryJID(sender.getText()), sender)
422 465
423 def remove_cb(sender): 466 def remove_cb(sender):
424 """Callback for the button to remove this contact.""" 467 """Callback for the button to remove this item."""
425 self._parent.addToRemainingList(sender.getText()) 468 self.manager.addToRemainingList([tryJID(sender.getText())])
426 self._parent.setRemainingListUnsorted() 469 self.manager.setRemainingListUnsorted()
427 self._last_textbox.setFocus(True) 470 self._last_textbox.setFocus(True)
428 471
429 def drop_cb(sender, target): 472 def drop_cb(sender, target):
430 """Callback when the textbox is drag-n-dropped.""" 473 """Callback when the textbox is drag-n-dropped."""
431 parent = sender._parent 474 list_panel = sender.list_panel
432 if target != parent and target.addContact(sender.getText()): 475 if target != list_panel and target.addItem(tryJID(sender.getText())):
433 sender.removeOrReset() 476 sender.removeOrReset()
434 else: 477 else:
435 parent._parent.removeFromRemainingList(sender.getText()) 478 list_panel.manager.removeFromRemainingList([tryJID(sender.getText())])
436 479
437 events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} 480 events_cbs = {"focus": focus_cb, "validate": add_cb, "remove": remove_cb, "drop": drop_cb}
438 textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) 481 textbox = DragAutoCompleteTextBox(self, events_cbs, self.style)
439 self.add(textbox) 482 self.add(textbox)
440 if switchPrevious: 483 if switchPrevious:
441 textbox.setText(self._last_textbox.getText()) 484 textbox.setText(self._last_textbox.getText())
442 textbox.setValid(self._last_textbox.valid) 485 textbox.setValid(self._last_textbox.valid)
443 self._last_textbox.reset() 486 self._last_textbox.reset()
444 previous = self._last_textbox 487 previous = self._last_textbox
445 self._last_textbox = textbox 488 self._last_textbox = textbox
446 return previous if switchPrevious else textbox 489 return previous if switchPrevious else textbox
447 490
448 def _checkContact(self, contact, modify): 491 def _checkItem(self, item, modify):
449 """ 492 """
450 @param contact: the contact to check 493 @param item (object): the item to check
451 @param modify: True if the contact is being modified 494 @param modify (bool): True if the item is being modified
452 @return: 495 @return: int value defined by one of these constants:
453 - VALID if the contact is valid 496 - VALID if the item is valid
454 - INVALID if the contact is not valid but can be displayed 497 - INVALID if the item is not valid but can be displayed
455 - DELETE if the contact should not be displayed at all 498 - DELETE if the item should not be displayed at all
456 """ 499 """
457 def countItemInList(list_, item): 500 def count(list_, item):
458 """For some reason the built-in count function doesn't work...""" 501 # XXX: list.count in not implemented by pyjamas
459 count = 0 502 return len([elt for elt in list_ if elt == item])
460 for elem in list_: 503
461 if elem == item: 504 if not item:
462 count += 1
463 return count
464 if contact is None or contact == "":
465 return DELETE 505 return DELETE
466 if countItemInList(self.getContacts(), contact) > (1 if modify else 0): 506 if count(self.getItems(), item) > (1 if modify else 0):
467 return DELETE 507 return DELETE
468 return VALID if contact in self._parent.list else INVALID 508 return VALID if item in self.manager.items else INVALID
469 509
470 def addContact(self, contact, sender=None): 510 def addItem(self, item, sender=None):
471 """The first parameter type is checked, so it is also possible to call addContact(sender). 511 """
472 If contact is not defined, sender.getText() is used. If sender is not defined, contact will 512
473 be written to the last textbox and a new textbox is added afterward. 513 @param item (object): item to be added
474 @param contact: unicode 514 @param (DragAutoCompleteTextBox): widget triggering the event
475 @param sender: DragAutoCompleteTextBox instance 515 @param sender: if True, the item will be "written" to the last textbox
476 """ 516 and a new text box will be added afterward.
477 if isinstance(contact, DragAutoCompleteTextBox): 517 """
478 sender = contact 518 valid = self._checkItem(item, sender is not None)
479 contact = sender.getText() 519 item_s = unicode(item)
480 valid = self._checkContact(contact, sender is not None)
481 if sender is None: 520 if sender is None:
482 # method has been called to modify but to add a contact 521 # method has been called not to modify but to add an item
483 if valid == VALID: 522 if valid == VALID:
484 # eventually insert before the last textbox if it's not empty 523 # eventually insert before the last textbox if it's not empty
485 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox 524 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox
486 sender.setText(contact) 525 sender.setText(item_s)
487 else: 526 else:
488 sender.setValid(valid == VALID) 527 sender.setValid(valid == VALID)
489 if valid != VALID: 528 if valid != VALID:
490 if sender is not None and valid == DELETE: 529 if sender is not None and valid == DELETE:
491 sender.removeOrReset() 530 sender.removeOrReset()
492 return False 531 return False
493 if sender == self._last_textbox: 532 if sender == self._last_textbox:
494 self._addTextBox() 533 self._addTextBox()
495 try: 534 sender.setVisibleLength(len(item_s))
496 sender.setVisibleLength(len(contact)) 535 self.manager.removeFromRemainingList([item])
497 except:
498 # IndexSizeError: Index or size is negative or greater than the allowed amount
499 log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact)))
500 self._parent.removeFromRemainingList(contact)
501 self._last_textbox.setFocus(True) 536 self._last_textbox.setFocus(True)
502 return True 537 return True
503 538
504 def emptyContacts(self): 539 def emptyItems(self):
505 """Empty the list of contacts.""" 540 """Empty the list of items."""
506 for child in self.getChildren(): 541 for child in self.getChildren():
507 if hasattr(child, "remove_btn"): 542 if hasattr(child, "remove_btn"):
508 child.remove_btn.click() 543 child.remove_btn.click()
509 544
510 def setContacts(self, tab): 545 def resetItems(self, items):
511 """Set the contacts.""" 546 """Repopulate the items.
512 self.emptyContacts() 547
513 if isinstance(tab, set): 548 @param items (list): the items to be listed.
514 tab = list(tab) 549 """
515 tab.sort() 550 self.emptyItems()
516 for contact in tab: 551 if isinstance(items, set):
517 self.addContact(contact) 552 items = list(items)
518 553 items.sort()
519 def getContacts(self): 554 for item in items:
520 """Get the contacts 555 self.addItem(item)
521 @return: an array of string""" 556
522 tab = [] 557 def getItems(self):
558 """Get the listed items.
559
560 @return: set"""
561 items = set()
523 for widget in self.getChildren(): 562 for widget in self.getChildren():
524 if isinstance(widget, DragAutoCompleteTextBox): 563 if isinstance(widget, DragAutoCompleteTextBox) and widget.getText() != "":
525 # not to be mixed with EMPTY_SELECTION_ITEM 564 items.add(tryJID(widget.getText()))
526 if widget.getText() != "": 565 return items
527 tab.append(widget.getText())
528 return tab
529
530 @property
531 def target_drop_cell(self):
532 """@return: the panel where something has been dropped."""
533 return self._parent.target_drop_cell
534
535 def setTargetDropCell(self, target_drop_cell):
536 """
537 XXX: Property setter here would not make it, you need a proper method!
538 @param target_drop_cell: the panel where something has been dropped."""
539 self._parent.setTargetDropCell(target_drop_cell)
540
541
542 class ContactChooserPanel(DialogBox):
543 """Display the contacts chooser dialog. This has been implemented while
544 prototyping and is currently not used. Left for an eventual later use.
545 Replaced by the popup menu which allows to add a panel for Cc or Bcc.
546 """
547
548 def __init__(self, manager, **kwargs):
549 """Display a listbox for each contact key"""
550 DialogBox.__init__(self, autoHide=False, centered=True, **kwargs)
551 self.setHTML("Select contacts")
552 self.manager = manager
553 self.listboxes = {}
554 self.contacts = manager.getContacts()
555
556 container = VerticalPanel(Visible=True)
557 container.addStyleName("marginAuto")
558
559 grid = Grid(2, len(self.manager.keys_dict))
560 index = -1
561 for key in self.manager.keys_dict:
562 index += 1
563 grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index)
564 listbox = ListBox()
565 listbox.setMultipleSelect(True)
566 listbox.setVisibleItemCount(15)
567 listbox.addItem(EMPTY_SELECTION_ITEM)
568 for element in manager.list:
569 listbox.addItem(element)
570 self.listboxes[key] = listbox
571 grid.add(listbox, 1, index)
572 self._reset()
573
574 buttons = HorizontalPanel()
575 buttons.addStyleName("marginAuto")
576 btn_close = Button("Cancel", self.hide)
577 buttons.add(btn_close)
578 btn_reset = Button("Reset", self._reset)
579 buttons.add(btn_reset)
580 btn_ok = Button("OK", self._validate)
581 buttons.add(btn_ok)
582
583 container.add(grid)
584 container.add(buttons)
585
586 self.add(container)
587 self.center()
588
589 def _reset(self):
590 """Reset the selections."""
591 for key in self.manager.keys_dict:
592 listbox = self.listboxes[key]
593 for i in xrange(0, listbox.getItemCount()):
594 if listbox.getItemText(i) in self.contacts[key]:
595 listbox.setItemSelected(i, "selected")
596 else:
597 listbox.setItemSelected(i, "")
598
599 def _validate(self):
600 """Sets back the selected contacts to the good sub-panels."""
601 _map = {}
602 for key in self.manager.keys_dict:
603 selections = self.listboxes[key].getSelectedItemText()
604 if EMPTY_SELECTION_ITEM in selections:
605 selections.remove(EMPTY_SELECTION_ITEM)
606 _map[key] = selections
607 self.manager.setContacts(_map)
608 self.hide()