Mercurial > libervia-web
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() |