Mercurial > libervia-web
comparison src/browser/sat_browser/list_manager.py @ 736:fe3c2357a8c9
fixes/improve ListManager and contact group manager + better PEP-8 compliance
author | souliane <souliane@mailoo.org> |
---|---|
date | Thu, 19 Nov 2015 11:41:03 +0100 |
parents | 9877607c719a |
children | 4545d48dee60 |
comparison
equal
deleted
inserted
replaced
735:e4ae8e2b0afd | 736:fe3c2357a8c9 |
---|---|
16 | 16 |
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 from pyjamas.ui.DragHandler import DragHandler | |
21 log = getLogger(__name__) | 22 log = getLogger(__name__) |
23 | |
24 from pyjamas.ui.ClickListener import ClickHandler | |
25 from pyjamas.ui.FocusListener import FocusHandler | |
26 from pyjamas.ui.ChangeListener import ChangeHandler | |
27 from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER | |
28 from pyjamas.ui.DragWidget import DragWidget | |
29 from pyjamas.ui.ListBox import ListBox | |
22 from pyjamas.ui.Button import Button | 30 from pyjamas.ui.Button import Button |
23 from pyjamas.ui.FlowPanel import FlowPanel | 31 from pyjamas.ui.FlowPanel import FlowPanel |
32 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
33 from pyjamas.ui.FlexTable import FlexTable | |
24 from pyjamas.ui.AutoComplete import AutoCompleteTextBox | 34 from pyjamas.ui.AutoComplete import AutoCompleteTextBox |
25 from pyjamas.ui.KeyboardListener import KEY_ENTER | |
26 from pyjamas.ui.DragWidget import DragWidget | |
27 from pyjamas.Timer import Timer | |
28 | 35 |
29 import base_panel | 36 import base_panel |
30 import base_widget | 37 import base_widget |
31 import libervia_widget | 38 import libervia_widget |
32 | 39 |
33 from sat_frontends.tools import jid | 40 from sat_frontends.quick_frontend import quick_list_manager |
34 | 41 |
35 | 42 |
36 unicode = str # FIXME: pyjamas workaround | 43 unicode = str # FIXME: pyjamas workaround |
37 | 44 |
38 # HTML content for the removal button (image or text) | 45 |
39 REMOVE_BUTTON = '<span class="itemRemoveIcon">x</span>' | 46 class ListItem(HorizontalPanel): |
40 | 47 """This class implements a list item with auto-completion and a delete button.""" |
41 | 48 |
42 # FIXME: dirty method and magic string to fix ASAP | 49 STYLE = {"listItem-box": "listItem-box", |
43 def tryJID(obj): | 50 "listItem-box-invalid": "listItem-box-invalid", |
44 return jid.JID(obj) if (isinstance(obj, unicode) and not obj.startswith('@')) else obj | 51 "listItem-button": "listItem-button", |
45 | 52 } |
46 | 53 |
47 class ListManager(object): | 54 VALID = 1 |
48 """A base class to manage one or several lists of items.""" | 55 INVALID = 2 |
49 | 56 DUPLICATE = 3 |
50 def __init__(self, container, keys=None, items=None, offsets=None, style=None): | 57 |
51 """ | 58 def __init__(self, listener=None, taglist=None, validate=None): |
52 @param container (FlexTable): FlexTable parent widget | 59 """ |
53 @param keys (dict{unicode: dict{unicode: unicode}}): dict binding items | 60 |
54 keys to their display config data. | 61 @param listener (ListItemHandler): handler for the UI events |
55 @param items (list): list of items | 62 @param taglist (quick_list_manager.QuickTagList): list manager |
56 @param offsets (dict): define widgets positions offsets within container: | 63 @param validate (callable): method returning a bool to validate the entry |
57 - "x_first": the x offset for the first widget's row on the grid | 64 """ |
58 - "x": the x offset for all widgets rows, except the first one if "x_first" is defined | 65 HorizontalPanel.__init__(self) |
59 - "y": the y offset for all widgets columns on the grid | 66 |
60 @param style (dict): define CSS styles | 67 self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"]) |
61 """ | 68 self.remove_btn = Button('<span>x</span>', Visible=False) |
62 self.container = container | 69 self.remove_btn.setStyleName(self.STYLE["listItem-button"]) |
63 self.keys = {} if keys is None else keys | 70 self.add(self.box) |
64 self.items = [] if items is None else items | 71 self.add(self.remove_btn) |
65 self.items.sort() | 72 |
66 | 73 if listener: |
67 # store the list of items that are not assigned yet | 74 self.box.addFocusListener(listener) |
68 self.items_remaining = [item for item in self.items] | 75 self.box.addChangeListener(listener) |
69 self.items_remaining_sorted = True | 76 self.box.addKeyboardListener(listener) |
70 | 77 self.box.choices.addClickListener(listener) |
71 self.offsets = {"x_first": 0, "x": 0, "y": 0} | 78 self.remove_btn.addClickListener(listener) |
72 if offsets is not None: | 79 |
73 if "x" in offsets and "x_first" not in offsets: | 80 self.taglist = taglist |
74 offsets["x_first"] = offsets["x"] | 81 self.validate = validate |
75 self.offsets.update(offsets) | 82 self.last_checked_value = "" |
76 | 83 self.last_validity = self.VALID |
77 self.style = {"keyItem": "itemKey", | |
78 "popupMenuItem": "itemKey", | |
79 "buttonCell": "itemButtonCell", | |
80 "keyPanel": "itemPanel", | |
81 "textBox": "itemTextBox", | |
82 "textBox-invalid": "itemTextBox-invalid", | |
83 "removeButton": "itemRemoveButton", | |
84 } | |
85 if style is not None: | |
86 self.style.update(style) | |
87 | |
88 def createWidgets(self, title_format="%s"): | |
89 """Fill the container widget with one ListPanel per item key (some may be | |
90 hidden during the initialization). | |
91 | |
92 @param title_format (unicode): format string for the title | |
93 """ | |
94 self.children = {} | |
95 for key in self.keys: | |
96 self.addItemKey(key, title_format=title_format) | |
97 | |
98 def addItemKey(self, key, data=None, title_format="%s"): | |
99 """Add to the container a Button and ListPanel for a new item key. | |
100 | |
101 @param key (unicode): item key | |
102 @param data (dict{unicode: unicode}): config data | |
103 """ | |
104 key_data = self.keys.setdefault(key, {}) | |
105 if data is not None: | |
106 key_data.update(data) | |
107 key_data["title"] = key # copy the key to its associated sub-map | |
108 | |
109 button = Button(title_format % key) | |
110 button.setStyleName(self.style["keyItem"]) | |
111 if hasattr(key_data, "desc"): | |
112 button.setTitle(key_data["desc"]) | |
113 if "optional" not in key_data: | |
114 key_data["optional"] = False | |
115 button.setVisible(not key_data["optional"]) | |
116 y = len(self.children) + self.offsets["y"] | |
117 x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] | |
118 | |
119 self.container.insertRow(y) | |
120 self.container.setWidget(y, x, button) | |
121 self.container.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) | |
122 | |
123 _child = ListPanel(self, key_data, self.style) | |
124 self.container.setWidget(y, x + 1, _child) | |
125 | |
126 self.children[key] = {} | |
127 self.children[key]["button"] = button | |
128 self.children[key]["panel"] = _child | |
129 | |
130 if hasattr(self, "popup_menu"): | |
131 # self.registerPopupMenuPanel has been called yet | |
132 self.popup_menu.registerClickSender(button) | |
133 | |
134 def removeItemKey(self, key): | |
135 """Remove from the container a ListPanel representing an item key, and all | |
136 its associated data. | |
137 | |
138 @param key (unicode): item key | |
139 """ | |
140 items = self.children[key]["panel"].getItems() | |
141 (y, x) = self.container.getIndex(self.children[key]["button"]) | |
142 self.container.removeRow(y) | |
143 del self.children[key] | |
144 del self.keys[key] | |
145 self.addToRemainingList(items) | |
146 | |
147 def refresh(self, hide_everything=False): | |
148 """Set visible the sub-panels that are non optional or non empty, hide | |
149 the rest. Setting the attribute "hide_everything" to True you can also | |
150 hide everything. | |
151 | |
152 @param hide_everything (boolean): set to True to hide everything | |
153 """ | |
154 for key in self.children: | |
155 self.setItemPanelVisible(key, False) | |
156 if hide_everything: | |
157 return | |
158 for key, items in self.getItemsByKey().iteritems(): | |
159 if len(items) > 0 or not self.keys[key]["optional"]: | |
160 self.setItemPanelVisible(key, True) | |
161 | |
162 def setVisible(self, visible): | |
163 self.refresh(not visible) | |
164 | |
165 def setItemPanelVisible(self, key, visible=True, sender=None): | |
166 """Set the item key's widgets visibility. | |
167 | |
168 @param key (unicode): item key | |
169 @param visible (bool): set to True to display the widgets | |
170 @param sender | |
171 """ | |
172 self.children[key]["button"].setVisible(visible) | |
173 self.children[key]["panel"].setVisible(visible) | |
174 | 84 |
175 @property | 85 @property |
176 def items_remaining(self): | 86 def text(self): |
177 """Return the unused items.""" | 87 return self.box.getText() |
178 if not self.items_remaining_sorted: | 88 |
179 self.items_remaining.sort() | 89 def setText(self, text): |
180 self.items_remaining_sorted = True | 90 """ |
181 return self.items_remaining | 91 Set the text and refresh the Widget. |
182 | 92 |
183 def setRemainingListUnsorted(self): | 93 @param text (unicode): text to set |
184 """Mark the list of unused items as being unsorted.""" | 94 """ |
185 self.items_remaining_sorted = False | 95 self.box.setText(text) |
186 | 96 self.refresh() |
187 def removeFromRemainingList(self, items): | 97 |
188 """Remove some items from the list of unused items. | 98 def refresh(self): |
189 | 99 if self.last_checked_value == self.text: |
190 @param items (list): items to be removed | 100 return |
191 """ | 101 |
102 if self.taglist and self.last_checked_value: | |
103 self.taglist.untag([self.last_checked_value]) | |
104 | |
105 if self.validate: # if None, the state is always valid | |
106 self.last_validity = self.validate(self.text) | |
107 | |
108 if self.last_validity == self.VALID: | |
109 self.box.removeStyleName(self.STYLE["listItem-box-invalid"]) | |
110 elif self.last_validity == self.INVALID: | |
111 self.box.addStyleName(self.STYLE["listItem-box-invalid"]) | |
112 elif self.last_validity == self.DUPLICATE: | |
113 self.remove_btn.click() # this may do more stuff then self.remove() | |
114 return | |
115 | |
116 if self.taglist and self.text: | |
117 self.taglist.tag([self.text]) | |
118 self.last_checked_value = self.text | |
119 self.remove_btn.setVisible(len(self.text) > 0) | |
120 | |
121 def setFocus(self, focused): | |
122 self.box.setFocus(focused) | |
123 | |
124 def remove(self): | |
125 """Remove the list item from its parent.""" | |
126 self.removeFromParent() | |
127 | |
128 if self.taglist and self.text: # this must be done after the widget has been removed | |
129 self.taglist.untag([self.text]) | |
130 | |
131 | |
132 class DraggableListItem(ListItem, DragWidget): | |
133 """This class is like ListItem, but in addition it can be dragged.""" | |
134 | |
135 def __init__(self, listener=None, taglist=None, validate=None): | |
136 """ | |
137 | |
138 @param listener (ListItemHandler): handler for the UI events | |
139 @param taglist (quick_list_manager.QuickTagList): list manager | |
140 @param validate (callable): method returning a bool to validate the entry | |
141 """ | |
142 ListItem.__init__(self, listener, taglist, validate) | |
143 DragWidget.__init__(self) | |
144 self.addDragListener(listener) | |
145 | |
146 | |
147 def onDragStart(self, event): | |
148 """The user starts dragging the item.""" | |
149 self.box.setSelectionRange(len(self.text), 0) | |
150 | |
151 dt = event.dataTransfer | |
152 dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX")) | |
153 dt.setDragImage(self.box.getElement(), 15, 15) | |
154 | |
155 | |
156 class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler): | |
157 """Implements basic handlers for the ListItem events.""" | |
158 | |
159 last_item = None # the last item is an empty text box for user input | |
160 | |
161 def __init__(self, manager, key): | |
162 ClickHandler.__init__(self) | |
163 FocusHandler.__init__(self) | |
164 ChangeHandler.__init__(self) | |
165 KeyboardHandler.__init__(self) | |
166 self.manager = manager | |
167 self.key = key | |
168 | |
169 def addItem(self, item): | |
170 raise NotImplementedError | |
171 | |
172 def removeItem(self, item): | |
173 raise NotImplementedError | |
174 | |
175 def onClick(self, sender): | |
176 """The remove button or a suggested completion item has been clicked.""" | |
177 #log.debug("onClick sender type: %s" % type(sender)) | |
178 if isinstance(sender, Button): | |
179 item = sender.getParent() | |
180 self.removeItem(item) | |
181 elif isinstance(sender, ListBox): | |
182 # this is called after onChange when you click a suggested item, and now we get the final value | |
183 textbox = sender._clickListeners[0] | |
184 self.checkValue(textbox) | |
185 else: | |
186 raise AssertionError | |
187 | |
188 def onFocus(self, sender): | |
189 """The text box has the focus.""" | |
190 #log.debug("onFocus sender type: %s" % type(sender)) | |
191 assert isinstance(sender, AutoCompleteTextBox) | |
192 sender.setCompletionItems(self.manager.untagged) | |
193 | |
194 def onKeyUp(self, sender, keycode, modifiers): | |
195 """The text box is being modified - or ENTER key has been pressed.""" | |
196 # this is called after onChange when you press ENTER, and now we get the final value | |
197 #log.debug("onKeyUp sender type: %s" % type(sender)) | |
198 assert isinstance(sender, AutoCompleteTextBox) | |
199 if keycode == KEY_ENTER: | |
200 self.checkValue(sender) | |
201 | |
202 def onChange(self, sender): | |
203 """The text box has been changed by the user.""" | |
204 # this is called before the completion when you press ENTER or click a suggest item | |
205 #log.debug("onChange sender type: %s" % type(sender)) | |
206 assert isinstance(sender, AutoCompleteTextBox) | |
207 self.checkValue(sender) | |
208 | |
209 def checkValue(self, textbox): | |
210 """Internal handler to call when a new value is submitted by the user.""" | |
211 item = textbox.getParent() | |
212 if item.text == item.last_checked_value: | |
213 # this method has already been called (by self.onChange) and there's nothing new | |
214 return | |
215 item.refresh() | |
216 item.box.setSelectionRange(len(item.text), 0) | |
217 if item == self.last_item and item.last_validity == ListItem.VALID and item.text: | |
218 self.addItem() | |
219 | |
220 class DraggableListItemHandler(ListItemHandler, DragHandler): | |
221 """Implements basic handlers for the DraggableListItem events.""" | |
222 | |
223 def __init__(self, manager, key): | |
224 ListItemHandler.__init__(self, manager, key) | |
225 DragHandler.__init__(self) | |
226 | |
227 def onDragStart(self, event): | |
228 """The user starts dragging the item.""" | |
229 self.manager.drop_target = None | |
230 | |
231 def onDragEnd(self, event): | |
232 """The user dropped the list item.""" | |
233 text, dummy = libervia_widget.eventGetData(event) | |
234 target = self.manager.drop_target # self or another ListPanel | |
235 if text == "" or target is None: | |
236 return | |
237 if target != self: # move the item from self to target | |
238 target.addItem(text) | |
239 self.removeItem(self.getItem(text)) | |
240 | |
241 | |
242 class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell): | |
243 """Implements a list of items.""" | |
244 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented: | |
245 # - it can not be used with pyjamas.ui.Label | |
246 # - FlowPanel.insert doesn't work | |
247 | |
248 STYLE = {"listPanel": "listPanel"} | |
249 | |
250 def __init__(self, manager, key, items): | |
251 """Initialization with a button for the list name (key) and a DraggableListItem. | |
252 | |
253 @param manager (ListManager) | |
254 @param key (unicode): list name | |
255 @param items (list): items to append | |
256 """ | |
257 FlowPanel.__init__(self) | |
258 DraggableListItemHandler.__init__(self, manager, key) | |
259 libervia_widget.DropCell.__init__(self, None) | |
260 self.addStyleName(self.STYLE["listPanel"]) | |
261 self.manager = manager | |
262 items.sort() | |
263 self.addItem() | |
192 for item in items: | 264 for item in items: |
193 if item in self.items_remaining: | 265 self.addItem(unicode(item)) |
194 self.items_remaining.remove(item) | |
195 | |
196 def addToRemainingList(self, items, ignore_key=None): | |
197 """Add some items to the list of unused items. Check first if the | |
198 items are really not used in any ListPanel. | |
199 | |
200 @param items (list): items to be removed | |
201 @param ignore_key (unicode): item key to be ignored while checking | |
202 """ | |
203 items_assigned = set() | |
204 for key, current_items in self.getItemsByKey().iteritems(): | |
205 if ignore_key is not None and key == ignore_key: | |
206 continue | |
207 items_assigned.update(current_items) | |
208 for item in items: | |
209 if item not in self.items or item in self.items_remaining or item in items_assigned: | |
210 continue | |
211 self.items_remaining.append(item) | |
212 self.setRemainingListUnsorted() | |
213 | |
214 def resetItems(self, data={}): | |
215 """Repopulate all the lists (one per item key) with the given items. | |
216 | |
217 @param data (dict{unicode: list}): dict binding items keys to items. | |
218 """ | |
219 for key in self.keys: | |
220 if key in data: | |
221 self.children[key]["panel"].resetItems(data[key]) | |
222 else: | |
223 self.children[key]["panel"].resetItems([]) | |
224 self.refresh() | |
225 | |
226 def getItemsByKey(self): | |
227 """Get all the items by key. | |
228 | |
229 @return: dict{unicode: set} | |
230 """ | |
231 return {key: self.children[key]["panel"].getItems() for key in self.children} | |
232 | |
233 def getKeysByItem(self): | |
234 """Get all the keys by item. | |
235 | |
236 @return: dict{object: set(unicode)} | |
237 """ | |
238 result = {} | |
239 for key in self.children: | |
240 for item in self.children[key]["panel"].getItems(): | |
241 result.setdefault(item, set()).add(key) | |
242 return result | |
243 | |
244 def registerPopupMenuPanel(self, entries, hide, callback): | |
245 """Register a popup menu panel for the item keys buttons. | |
246 | |
247 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries | |
248 @param hide (callable): method to call in order to know if a menu item | |
249 should be hidden from the menu. Takes in the button widget and the | |
250 item key and returns a boolean. | |
251 @param callback (callable): common callback for all menu items, takes in | |
252 the button widget and the item key. | |
253 """ | |
254 self.popup_menu = base_panel.PopupMenuPanel(entries, hide, callback, style={"item": self.style["popupMenuItem"]}) | |
255 | |
256 | |
257 class DragAutoCompleteTextBox(AutoCompleteTextBox, DragWidget): | |
258 """A draggable AutoCompleteTextBox which is used for representing an item.""" | |
259 | |
260 def __init__(self, list_panel, event_cbs, style): | |
261 """ | |
262 | |
263 @param list_panel (ListPanel) | |
264 @param event_cbs (list[callable]) | |
265 @param style (dict) | |
266 """ | |
267 AutoCompleteTextBox.__init__(self) | |
268 DragWidget.__init__(self) | |
269 self.list_panel = list_panel | |
270 self.event_cbs = event_cbs | |
271 self.style = style | |
272 self.addStyleName(style["textBox"]) | |
273 self.reset() | |
274 | |
275 # Parent classes already init self as an handler for these events | |
276 self.addMouseListener(self) | |
277 self.addFocusListener(self) | |
278 self.addChangeListener(self) | |
279 | |
280 def onDragStart(self, event): | |
281 """The user starts dragging the text box.""" | |
282 self.list_panel.manager.target_drop_cell = None | |
283 self.setSelectionRange(len(self.getText()), 0) | |
284 | |
285 dt = event.dataTransfer | |
286 dt.setData('text/plain', "%s\n%s" % (self.getText(), "CONTACT_TEXTBOX")) | |
287 dt.setDragImage(self.getElement(), 15, 15) | |
288 | |
289 def onDragEnd(self, event): | |
290 """The user dropped the text box.""" | |
291 target = self.list_panel.manager.target_drop_cell # parent or another ListPanel | |
292 if self.getText() == "" or target is None: | |
293 return | |
294 self.event_cbs["drop"](self, target) | |
295 | |
296 def onClick(self, sender): | |
297 """The choices list is clicked""" | |
298 assert sender == self.choices | |
299 AutoCompleteTextBox.onClick(self, sender) | |
300 self.validate() | |
301 | |
302 def onChange(self, sender): | |
303 """The list selection or the text has been changed""" | |
304 assert sender == self.choices or sender == self | |
305 if sender == self.choices: | |
306 AutoCompleteTextBox.onChange(self, sender) | |
307 self.validate() | |
308 | |
309 def onKeyUp(self, sender, keycode, modifiers): | |
310 """Listen for key stroke""" | |
311 assert sender == self | |
312 AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) | |
313 if keycode == KEY_ENTER: | |
314 self.validate() | |
315 | |
316 def onMouseMove(self, sender): | |
317 """Mouse enters the area of a DragAutoCompleteTextBox.""" | |
318 assert sender == self | |
319 if hasattr(sender, "remove_btn"): | |
320 sender.remove_btn.setVisible(True) | |
321 | |
322 def onMouseLeave(self, sender): | |
323 """Mouse leaves the area of a DragAutoCompleteTextBox.""" | |
324 assert sender == self | |
325 if hasattr(sender, "remove_btn"): | |
326 Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) | |
327 | |
328 def onFocus(self, sender): | |
329 """The DragAutoCompleteTextBox has the focus.""" | |
330 assert sender == self | |
331 # FIXME: this raises runtime JS error "Permission denied to access property..." when you drag the object | |
332 #sender.setSelectionRange(0, len(sender.getText())) | |
333 sender.event_cbs["focus"](sender) | |
334 | |
335 def reset(self): | |
336 """Reset the text box""" | |
337 self.setText("") | |
338 self.setValid() | |
339 | |
340 def setValid(self, valid=True): | |
341 """Change the style according to the text validity.""" | |
342 if self.getText() == "": | |
343 valid = True | |
344 if valid: | |
345 self.removeStyleName(self.style["textBox-invalid"]) | |
346 else: | |
347 self.addStyleName(self.style["textBox-invalid"]) | |
348 self.valid = valid | |
349 | |
350 def validate(self): | |
351 """Check if the text is valid, update the style.""" | |
352 self.setSelectionRange(len(self.getText()), 0) | |
353 self.event_cbs["validate"](self) | |
354 | |
355 def setRemoveButton(self): | |
356 """Add the remove button after the text box.""" | |
357 | |
358 def remove_cb(sender): | |
359 """Callback for the button to remove this item.""" | |
360 self.list_panel.remove(self) | |
361 self.list_panel.remove(self.remove_btn) | |
362 self.event_cbs["remove"](self) | |
363 | |
364 self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) | |
365 self.remove_btn.setStyleName(self.style["removeButton"]) | |
366 self.list_panel.add(self.remove_btn) | |
367 | |
368 def removeOrReset(self): | |
369 """Remove the text box if the remove button exists, or reset the text box.""" | |
370 if hasattr(self, "remove_btn"): | |
371 self.remove_btn.click() | |
372 else: | |
373 self.reset() | |
374 | |
375 | |
376 VALID = 1 | |
377 INVALID = 2 | |
378 DELETE = 3 | |
379 | |
380 | |
381 class ListPanel(FlowPanel, libervia_widget.DropCell): | |
382 """Panel used for listing items sharing the same key. The key is showed as | |
383 a Button to which you can bind a popup menu and the items are represented | |
384 with a sequence of DragAutoCompleteTextBox.""" | |
385 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented yet and can not be used with pyjamas.ui.Label | |
386 | |
387 def __init__(self, manager, data, style={}): | |
388 """Initialization with a button and a DragAutoCompleteTextBox. | |
389 | |
390 @param manager (ListManager) | |
391 @param data (dict{unicode: unicode}) | |
392 @param style (dict{unicode: unicode}) | |
393 """ | |
394 FlowPanel.__init__(self, Visible=(False if data["optional"] else True)) | |
395 | |
396 def setTargetDropCell(host, item): | |
397 self.manager.target_drop_cell = self | |
398 | 266 |
399 # FIXME: dirty magic strings '@' and '@@' | 267 # FIXME: dirty magic strings '@' and '@@' |
400 drop_cbs = {"GROUP": lambda host, item: self.addItem("@%s" % item), | 268 self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s), |
401 "CONTACT": lambda host, item: self.addItem(tryJID(item)), | 269 "CONTACT": lambda host, item_s: self.addItem(item_s), |
402 "CONTACT_TITLE": lambda host, item: self.addItem('@@'), | 270 "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'), |
403 "CONTACT_TEXTBOX": setTargetDropCell | 271 "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self), |
404 } | 272 } |
405 libervia_widget.DropCell.__init__(self, None) | |
406 self.drop_keys = drop_cbs | |
407 self.style = style | |
408 self.addStyleName(self.style["keyPanel"]) | |
409 self.manager = manager | |
410 self.key = data["title"] | |
411 self._addTextBox() | |
412 | 273 |
413 def onDrop(self, event): | 274 def onDrop(self, event): |
275 """Something has been dropped in this ListPanel""" | |
414 try: | 276 try: |
415 libervia_widget.DropCell.onDrop(self, event) | 277 libervia_widget.DropCell.onDrop(self, event) |
416 except base_widget.NoLiberviaWidgetException: | 278 except base_widget.NoLiberviaWidgetException: |
417 pass | 279 pass |
418 | 280 |
419 def _addTextBox(self, switchPrevious=False): | 281 def getItem(self, text): |
420 """Add an empty text box to the last position. | 282 """Get an item from its text. |
421 | 283 |
422 @param switchPrevious (bool): if True, simulate an insertion before the | 284 @param text(unicode): item text |
423 current last textbox by switching the texts and valid states | 285 """ |
424 @return: an DragAutoCompleteTextBox, the created text box or the | 286 for child in self.getChildren(): |
425 previous one if switchPrevious is True. | 287 if child.text == text: |
426 """ | 288 return child |
427 if hasattr(self, "_last_textbox"): | 289 return None |
428 if self._last_textbox.getText() == "": | 290 |
429 return | 291 def getItems(self): |
430 self._last_textbox.setRemoveButton() | 292 """Get the non empty items. |
431 else: | 293 |
432 switchPrevious = False | 294 @return list(unicode) |
433 | 295 """ |
434 def focus_cb(sender): | 296 return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text] |
435 if sender != self._last_textbox: | 297 |
436 # save the current value before it's being modified | 298 def validateItem(self, text): |
437 self.manager.addToRemainingList([tryJID(sender.getText())], ignore_key=self.key) | 299 """Return validation code after the item has been changed. |
438 | 300 |
439 items = [unicode(item) for item in self.manager.items_remaining] | 301 @param text (unicode): item text to check |
440 sender.setCompletionItems(items) | |
441 | |
442 def add_cb(sender): | |
443 self.addItem(tryJID(sender.getText()), sender) | |
444 | |
445 def remove_cb(sender): | |
446 """Callback for the button to remove this item.""" | |
447 self.manager.addToRemainingList([tryJID(sender.getText())]) | |
448 self.manager.setRemainingListUnsorted() | |
449 self._last_textbox.setFocus(True) | |
450 | |
451 def drop_cb(sender, target): | |
452 """Callback when the textbox is drag-n-dropped.""" | |
453 list_panel = sender.list_panel | |
454 if target != list_panel and target.addItem(tryJID(sender.getText())): | |
455 sender.removeOrReset() | |
456 else: | |
457 list_panel.manager.removeFromRemainingList([tryJID(sender.getText())]) | |
458 | |
459 events_cbs = {"focus": focus_cb, "validate": add_cb, "remove": remove_cb, "drop": drop_cb} | |
460 textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) | |
461 self.add(textbox) | |
462 if switchPrevious: | |
463 textbox.setText(self._last_textbox.getText()) | |
464 textbox.setValid(self._last_textbox.valid) | |
465 self._last_textbox.reset() | |
466 previous = self._last_textbox | |
467 self._last_textbox = textbox | |
468 return previous if switchPrevious else textbox | |
469 | |
470 def _checkItem(self, item, modify): | |
471 """ | |
472 @param item (object): the item to check | |
473 @param modify (bool): True if the item is being modified | |
474 @return: int value defined by one of these constants: | 302 @return: int value defined by one of these constants: |
475 - VALID if the item is valid | 303 - VALID if the item is valid |
476 - INVALID if the item is not valid but can be displayed | 304 - INVALID if the item is not valid but can be displayed |
477 - DELETE if the item should not be displayed at all | 305 - DUPLICATE if the item is a duplicate |
478 """ | 306 """ |
479 def count(list_, item): | 307 def count(list_, item): # XXX: list.count in not implemented by pyjamas |
480 # XXX: list.count in not implemented by pyjamas | |
481 return len([elt for elt in list_ if elt == item]) | 308 return len([elt for elt in list_ if elt == item]) |
482 | 309 |
483 if not item: | 310 if count(self.getItems(), text) > 1: |
484 return DELETE | 311 return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion |
485 if count(self.getItems(), item) > (1 if modify else 0): | 312 return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID |
486 return DELETE | 313 |
487 return VALID if item in self.manager.items else INVALID | 314 def addItem(self, text=""): |
488 | 315 """Add an item. |
489 def addItem(self, item, sender=None): | 316 |
490 """Try to add an item. It will be added if it's a valid one. | 317 @param text (unicode): text to be set. |
491 | 318 @return: True if the item has been really added or merged. |
492 @param item (object): item to be added | 319 """ |
493 @param (DragAutoCompleteTextBox): widget triggering the event | 320 if text in self.getItems(): # avoid duplicate in the same list |
494 @param sender: if True, the item will be "written" to the last textbox | 321 return |
495 and a new text box will be added afterward. | 322 |
496 @return: True if the item has been added. | 323 item = DraggableListItem(self, self.manager, self.validateItem) |
497 """ | 324 self.add(item) |
498 valid = self._checkItem(item, sender is not None) | 325 |
499 item_s = unicode(item) | 326 if self.last_item: |
500 if sender is None: | 327 if self.last_item.last_validity == ListItem.INVALID: |
501 # method has been called not to modify but to add an item | 328 # switch the two values so that the invalid one stays in last position |
502 if valid == VALID: | 329 item.setText(self.last_item.text) |
503 # eventually insert before the last textbox if it's not empty | 330 self.last_item.setText(text) |
504 sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox | 331 elif not self.last_item.text: |
505 sender.setText(item_s) | 332 # copy the new value to previous empty item |
506 else: | 333 self.last_item.setText(text) |
507 sender.setValid(valid == VALID) | 334 else: # first item of the list, or previous last item has been deleted |
508 if valid != VALID: | 335 item.setText(text) |
509 if sender is not None and valid == DELETE: | 336 |
510 sender.removeOrReset() | 337 self.last_item = item |
511 return False | 338 self.last_item.setFocus(True) |
512 if sender == self._last_textbox: | 339 |
513 self._addTextBox() | 340 def removeItem(self, item): |
514 sender.setVisibleLength(len(item_s)) | 341 """Remove an item. |
515 self.manager.removeFromRemainingList([item]) | 342 |
516 self._last_textbox.setFocus(True) | 343 @param item(DraggableListItem): item to remove |
517 return True | 344 """ |
518 | 345 if item == self.last_item: |
519 def emptyItems(self): | 346 self.addItem("") |
520 """Empty the list of items.""" | 347 item.remove() # this also updates the taglist |
521 for child in self.getChildren(): | 348 |
522 if hasattr(child, "remove_btn"): | 349 |
523 child.remove_btn.click() | 350 class ListManager(FlexTable, quick_list_manager.QuickTagList): |
524 | 351 """Implements a table to manage one or several lists of items.""" |
525 def resetItems(self, items): | 352 |
526 """Repopulate the items. | 353 STYLE = {"listManager-button": "group", |
527 | 354 "listManager-button-cell": "listManager-button-cell", |
528 @param items (list): the items to be listed. | 355 } |
529 """ | 356 |
530 self.emptyItems() | 357 def __init__(self, data=None, items=None): |
531 if isinstance(items, set): | 358 """ |
532 items = list(items) | 359 @param data (dict{unicode: list}): dict binding keys to tagged items. |
533 items.sort() | 360 @param items (list): full list of items (tagged and untagged) |
534 for item in items: | 361 """ |
535 self.addItem(item) | 362 FlexTable.__init__(self, Width="100%") |
536 | 363 quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items]) |
537 def getItems(self): | 364 self.lists = {} |
538 """Get the listed items. | 365 |
539 | 366 if data: |
540 @return: set""" | 367 for key, items in data.iteritems(): |
541 items = set() | 368 self.addList(key, [unicode(item) for item in items]) |
542 for widget in self.getChildren(): | 369 |
543 if isinstance(widget, DragAutoCompleteTextBox) and widget.getText() != "": | 370 def addList(self, key, items=None): |
544 items.add(tryJID(widget.getText())) | 371 """Add a Button and ListPanel for a new list. |
545 return items | 372 |
373 @param key (unicode): list name | |
374 @param items (list): items to append to the new list | |
375 """ | |
376 if key in self.lists: | |
377 return | |
378 | |
379 if items is None: | |
380 items = [] | |
381 | |
382 self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]), | |
383 "panel": ListPanel(self, key, items)} | |
384 | |
385 y, x = len(self.lists), 0 | |
386 self.insertRow(y) | |
387 self.setWidget(y, x, self.lists[key]["button"]) | |
388 self.setWidget(y, x + 1, self.lists[key]["panel"]) | |
389 self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"]) | |
390 | |
391 try: | |
392 self.popup_menu.registerClickSender(self.lists[key]["button"]) | |
393 except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet | |
394 pass | |
395 | |
396 def removeList(self, key): | |
397 """Remove a ListPanel from this manager. | |
398 | |
399 @param key (unicode): list name | |
400 """ | |
401 items = self.lists[key]["panel"].getItems() | |
402 (y, x) = self.getIndex(self.lists[key]["button"]) | |
403 self.removeRow(y) | |
404 del self.lists[key] | |
405 self.untag(items) | |
406 | |
407 def untag(self, items): | |
408 """Untag some items. | |
409 | |
410 Check first if the items are not used in any panel. | |
411 | |
412 @param items (list): items to be removed | |
413 """ | |
414 items_assigned = set() | |
415 for values in self.getItemsByKey().itervalues(): | |
416 items_assigned.update(values) | |
417 quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned]) | |
418 | |
419 def getItemsByKey(self): | |
420 """Get the items grouped by list name. | |
421 | |
422 @return dict{unicode: list} | |
423 """ | |
424 return {key: self.lists[key]["panel"].getItems() for key in self.lists} | |
425 | |
426 def getKeysByItem(self): | |
427 """Get the keys groups by item. | |
428 | |
429 @return dict{object: set(unicode)} | |
430 """ | |
431 result = {} | |
432 for key in self.lists: | |
433 for item in self.lists[key]["panel"].getItems(): | |
434 result.setdefault(item, set()).add(key) | |
435 return result | |
436 | |
437 def registerPopupMenuPanel(self, entries, callback): | |
438 """Register a popup menu panel for the list names' buttons. | |
439 | |
440 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries | |
441 @param callback (callable): common callback for all menu items, arguments are: | |
442 - button widget | |
443 - list name (item key) | |
444 """ | |
445 self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback) | |
446 for key in self.lists: # register click sender for already existing lists | |
447 self.popup_menu.registerClickSender(self.lists[key]["button"]) |