Mercurial > libervia-web
comparison browser/sat_browser/list_manager.py @ 1124:28e3eb3bb217
files reorganisation and installation rework:
- files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory)
- VERSION file is now used, as for other SàT projects
- replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly
- removed check for data_dir if it's empty
- installation tested working in virtual env
- libervia launching script is now in bin/libervia
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 25 Aug 2018 17:59:48 +0200 |
parents | src/browser/sat_browser/list_manager.py@f8a7a046ff9c |
children |
comparison
equal
deleted
inserted
replaced
1123:63a4b8fe9782 | 1124:28e3eb3bb217 |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Libervia: a Salut à Toi frontend | |
5 # Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
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/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 from sat.core.i18n import _ | |
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.DragHandler import DragHandler | |
28 from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER | |
29 from pyjamas.ui.DragWidget import DragWidget | |
30 from pyjamas.ui.ListBox import ListBox | |
31 from pyjamas.ui.Button import Button | |
32 from pyjamas.ui.FlowPanel import FlowPanel | |
33 from pyjamas.ui.HorizontalPanel import HorizontalPanel | |
34 from pyjamas.ui.FlexTable import FlexTable | |
35 from pyjamas.ui.AutoComplete import AutoCompleteTextBox | |
36 | |
37 import base_panel | |
38 import base_widget | |
39 import libervia_widget | |
40 | |
41 from sat_frontends.quick_frontend import quick_list_manager | |
42 | |
43 | |
44 unicode = str # FIXME: pyjamas workaround | |
45 | |
46 | |
47 class ListItem(HorizontalPanel): | |
48 """This class implements a list item with auto-completion and a delete button.""" | |
49 | |
50 STYLE = {"listItem": "listItem", | |
51 "listItem-box": "listItem-box", | |
52 "listItem-box-invalid": "listItem-box-invalid", | |
53 "listItem-button": "listItem-button", | |
54 } | |
55 | |
56 VALID = 1 | |
57 INVALID = 2 | |
58 DUPLICATE = 3 | |
59 | |
60 def __init__(self, listener=None, taglist=None, validate=None): | |
61 """ | |
62 | |
63 @param listener (ListItemHandler): handler for the UI events | |
64 @param taglist (quick_list_manager.QuickTagList): list manager | |
65 @param validate (callable): method returning a bool to validate the entry | |
66 """ | |
67 HorizontalPanel.__init__(self) | |
68 self.addStyleName(self.STYLE["listItem"]) | |
69 | |
70 self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"]) | |
71 self.remove_btn = Button('<span>x</span>', Visible=False) | |
72 self.remove_btn.setStyleName(self.STYLE["listItem-button"]) | |
73 self.add(self.box) | |
74 self.add(self.remove_btn) | |
75 | |
76 if listener: | |
77 self.box.addFocusListener(listener) | |
78 self.box.addChangeListener(listener) | |
79 self.box.addKeyboardListener(listener) | |
80 self.box.choices.addClickListener(listener) | |
81 self.remove_btn.addClickListener(listener) | |
82 | |
83 self.taglist = taglist | |
84 self.validate = validate | |
85 self.last_checked_value = "" | |
86 self.last_validity = self.VALID | |
87 | |
88 @property | |
89 def text(self): | |
90 return self.box.getText() | |
91 | |
92 def setText(self, text): | |
93 """ | |
94 Set the text and refresh the Widget. | |
95 | |
96 @param text (unicode): text to set | |
97 """ | |
98 self.box.setText(text) | |
99 self.refresh() | |
100 | |
101 def refresh(self): | |
102 if self.last_checked_value == self.text: | |
103 return | |
104 | |
105 if self.taglist and self.last_checked_value: | |
106 self.taglist.untag([self.last_checked_value]) | |
107 | |
108 if self.validate: # if None, the state is always valid | |
109 self.last_validity = self.validate(self.text) | |
110 | |
111 if self.last_validity == self.VALID: | |
112 self.box.removeStyleName(self.STYLE["listItem-box-invalid"]) | |
113 self.box.setVisibleLength(max(len(self.text), 10)) | |
114 elif self.last_validity == self.INVALID: | |
115 self.box.addStyleName(self.STYLE["listItem-box-invalid"]) | |
116 elif self.last_validity == self.DUPLICATE: | |
117 self.remove_btn.click() # this may do more stuff then self.remove() | |
118 return | |
119 | |
120 if self.taglist and self.text: | |
121 self.taglist.tag([self.text]) | |
122 self.last_checked_value = self.text | |
123 self.box.setSelectionRange(len(self.text), 0) | |
124 self.remove_btn.setVisible(len(self.text) > 0) | |
125 | |
126 def setFocus(self, focused): | |
127 self.box.setFocus(focused) | |
128 | |
129 def remove(self): | |
130 """Remove the list item from its parent.""" | |
131 self.removeFromParent() | |
132 | |
133 if self.taglist and self.text: # this must be done after the widget has been removed | |
134 self.taglist.untag([self.text]) | |
135 | |
136 | |
137 class DraggableListItem(ListItem, DragWidget): | |
138 """This class is like ListItem, but in addition it can be dragged.""" | |
139 | |
140 def __init__(self, listener=None, taglist=None, validate=None): | |
141 """ | |
142 | |
143 @param listener (ListItemHandler): handler for the UI events | |
144 @param taglist (quick_list_manager.QuickTagList): list manager | |
145 @param validate (callable): method returning a bool to validate the entry | |
146 """ | |
147 ListItem.__init__(self, listener, taglist, validate) | |
148 DragWidget.__init__(self) | |
149 self.addDragListener(listener) | |
150 | |
151 | |
152 def onDragStart(self, event): | |
153 """The user starts dragging the item.""" | |
154 dt = event.dataTransfer | |
155 dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX")) | |
156 dt.setDragImage(self.box.getElement(), 15, 15) | |
157 | |
158 | |
159 class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler): | |
160 """Implements basic handlers for the ListItem events.""" | |
161 | |
162 last_item = None # the last item is an empty text box for user input | |
163 | |
164 def __init__(self, taglist): | |
165 """ | |
166 | |
167 @param taglist (quick_list_manager.QuickTagList): list manager | |
168 """ | |
169 ClickHandler.__init__(self) | |
170 FocusHandler.__init__(self) | |
171 ChangeHandler.__init__(self) | |
172 KeyboardHandler.__init__(self) | |
173 self.taglist = taglist | |
174 | |
175 def addItem(self, item): | |
176 raise NotImplementedError | |
177 | |
178 def removeItem(self, item): | |
179 raise NotImplementedError | |
180 | |
181 def onClick(self, sender): | |
182 """The remove button or a suggested completion item has been clicked.""" | |
183 #log.debug("onClick sender type: %s" % type(sender)) | |
184 if isinstance(sender, Button): | |
185 item = sender.getParent() | |
186 self.removeItem(item) | |
187 elif isinstance(sender, ListBox): | |
188 # this is called after onChange when you click a suggested item, and now we get the final value | |
189 textbox = sender._clickListeners[0] | |
190 self.checkValue(textbox) | |
191 else: | |
192 raise AssertionError | |
193 | |
194 def onFocus(self, sender): | |
195 """The text box has the focus.""" | |
196 #log.debug("onFocus sender type: %s" % type(sender)) | |
197 assert isinstance(sender, AutoCompleteTextBox) | |
198 sender.setCompletionItems(self.taglist.untagged) | |
199 | |
200 def onKeyUp(self, sender, keycode, modifiers): | |
201 """The text box is being modified - or ENTER key has been pressed.""" | |
202 # this is called after onChange when you press ENTER, and now we get the final value | |
203 #log.debug("onKeyUp sender type: %s" % type(sender)) | |
204 assert isinstance(sender, AutoCompleteTextBox) | |
205 if keycode == KEY_ENTER: | |
206 self.checkValue(sender) | |
207 | |
208 def onChange(self, sender): | |
209 """The text box has been changed by the user.""" | |
210 # this is called before the completion when you press ENTER or click a suggest item | |
211 #log.debug("onChange sender type: %s" % type(sender)) | |
212 assert isinstance(sender, AutoCompleteTextBox) | |
213 self.checkValue(sender) | |
214 | |
215 def checkValue(self, textbox): | |
216 """Internal handler to call when a new value is submitted by the user.""" | |
217 item = textbox.getParent() | |
218 if item.text == item.last_checked_value: | |
219 # this method has already been called (by self.onChange) and there's nothing new | |
220 return | |
221 item.refresh() | |
222 if item == self.last_item and item.last_validity == ListItem.VALID and item.text: | |
223 self.addItem() | |
224 | |
225 class DraggableListItemHandler(ListItemHandler, DragHandler): | |
226 """Implements basic handlers for the DraggableListItem events.""" | |
227 | |
228 def __init__(self, manager): | |
229 """ | |
230 | |
231 @param manager (ListManager): list manager | |
232 """ | |
233 ListItemHandler.__init__(self, manager) | |
234 DragHandler.__init__(self) | |
235 | |
236 @property | |
237 def manager(self): | |
238 return self.taglist | |
239 | |
240 def onDragStart(self, event): | |
241 """The user starts dragging the item.""" | |
242 self.manager.drop_target = None | |
243 | |
244 def onDragEnd(self, event): | |
245 """The user dropped the list item.""" | |
246 text, dummy = libervia_widget.eventGetData(event) | |
247 target = self.manager.drop_target # self or another ListPanel | |
248 if text == "" or target is None: | |
249 return | |
250 if target != self: # move the item from self to target | |
251 target.addItem(text) | |
252 self.removeItem(self.getItem(text)) | |
253 | |
254 | |
255 class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell): | |
256 """Implements a list of items.""" | |
257 # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented: | |
258 # - it can not be used with pyjamas.ui.Label | |
259 # - FlowPanel.insert doesn't work | |
260 | |
261 STYLE = {"listPanel": "listPanel"} | |
262 ACCEPT_NEW_ENTRY = False | |
263 | |
264 def __init__(self, manager, items=None): | |
265 """Initialization with a button for the list name (key) and a DraggableListItem. | |
266 | |
267 @param manager (ListManager): list manager | |
268 @param items (list): items to be set | |
269 """ | |
270 FlowPanel.__init__(self) | |
271 DraggableListItemHandler.__init__(self, manager) | |
272 libervia_widget.DropCell.__init__(self, None) | |
273 self.addStyleName(self.STYLE["listPanel"]) | |
274 self.manager = manager | |
275 self.resetItems(items) | |
276 | |
277 # FIXME: dirty magic strings '@' and '@@' | |
278 self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s), | |
279 "CONTACT": lambda host, item_s: self.addItem(item_s), | |
280 "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'), | |
281 "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self), | |
282 } | |
283 | |
284 def onDrop(self, event): | |
285 """Something has been dropped in this ListPanel""" | |
286 try: | |
287 libervia_widget.DropCell.onDrop(self, event) | |
288 except base_widget.NoLiberviaWidgetException: | |
289 pass | |
290 | |
291 def getItem(self, text): | |
292 """Get an item from its text. | |
293 | |
294 @param text(unicode): item text | |
295 """ | |
296 for child in self.getChildren(): | |
297 if child.text == text: | |
298 return child | |
299 return None | |
300 | |
301 def getItems(self): | |
302 """Get the non empty items. | |
303 | |
304 @return list(unicode) | |
305 """ | |
306 return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text] | |
307 | |
308 def validateItem(self, text): | |
309 """Return validation code after the item has been changed. | |
310 | |
311 @param text (unicode): item text to check | |
312 @return: int value defined by one of these constants: | |
313 - VALID if the item is valid | |
314 - INVALID if the item is not valid but can be displayed | |
315 - DUPLICATE if the item is a duplicate | |
316 """ | |
317 def count(list_, item): # XXX: list.count in not implemented by pyjamas | |
318 return len([elt for elt in list_ if elt == item]) | |
319 | |
320 if count(self.getItems(), text) > 1: | |
321 return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion | |
322 if self.ACCEPT_NEW_ENTRY: | |
323 return ListItem.VALID | |
324 return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID | |
325 | |
326 def addItem(self, text=""): | |
327 """Add an item. | |
328 | |
329 @param text (unicode): text to be set. | |
330 @return: True if the item has been really added or merged. | |
331 """ | |
332 if text in self.getItems(): # avoid duplicate in the same list | |
333 return | |
334 | |
335 item = DraggableListItem(self, self.manager, self.validateItem) | |
336 self.add(item) | |
337 | |
338 if self.last_item: | |
339 if self.last_item.last_validity == ListItem.INVALID: | |
340 # switch the two values so that the invalid one stays in last position | |
341 item.setText(self.last_item.text) | |
342 self.last_item.setText(text) | |
343 elif not self.last_item.text: | |
344 # copy the new value to previous empty item | |
345 self.last_item.setText(text) | |
346 else: # first item of the list, or previous last item has been deleted | |
347 item.setText(text) | |
348 | |
349 self.last_item = item | |
350 self.last_item.setFocus(True) | |
351 | |
352 def removeItem(self, item): | |
353 """Remove an item. | |
354 | |
355 @param item(DraggableListItem): item to remove | |
356 """ | |
357 if item == self.last_item: | |
358 self.addItem("") | |
359 item.remove() # this also updates the taglist | |
360 | |
361 def resetItems(self, items): | |
362 """Reset the items. | |
363 | |
364 @param items (list): items to be set | |
365 """ | |
366 for child in self.getChildren(): | |
367 child.remove() | |
368 | |
369 self.addItem() | |
370 if not items: | |
371 return | |
372 | |
373 items.sort() | |
374 for item in items: | |
375 self.addItem(unicode(item)) | |
376 | |
377 | |
378 class ListManager(FlexTable, quick_list_manager.QuickTagList): | |
379 """Implements a table to manage one or several lists of items.""" | |
380 | |
381 STYLE = {"listManager-button": "group", | |
382 "listManager-button-cell": "listManager-button-cell", | |
383 } | |
384 | |
385 def __init__(self, data=None, items=None): | |
386 """ | |
387 @param data (dict{unicode: list}): dict binding keys to tagged items. | |
388 @param items (list): full list of items (tagged and untagged) | |
389 """ | |
390 FlexTable.__init__(self, Width="100%") | |
391 quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items]) | |
392 self.lists = {} | |
393 | |
394 if data: | |
395 for key, items in data.iteritems(): | |
396 self.addList(key, [unicode(item) for item in items]) | |
397 | |
398 def addList(self, key, items=None): | |
399 """Add a Button and ListPanel for a new list. | |
400 | |
401 @param key (unicode): list name | |
402 @param items (list): items to append to the new list | |
403 """ | |
404 if key in self.lists: | |
405 return | |
406 | |
407 if items is None: | |
408 items = [] | |
409 | |
410 self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]), | |
411 "panel": ListPanel(self, items)} | |
412 | |
413 y, x = len(self.lists), 0 | |
414 self.insertRow(y) | |
415 self.setWidget(y, x, self.lists[key]["button"]) | |
416 self.setWidget(y, x + 1, self.lists[key]["panel"]) | |
417 self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"]) | |
418 | |
419 try: | |
420 self.popup_menu.registerClickSender(self.lists[key]["button"]) | |
421 except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet | |
422 pass | |
423 | |
424 def removeList(self, key): | |
425 """Remove a ListPanel from this manager. | |
426 | |
427 @param key (unicode): list name | |
428 """ | |
429 items = self.lists[key]["panel"].getItems() | |
430 (y, x) = self.getIndex(self.lists[key]["button"]) | |
431 self.removeRow(y) | |
432 del self.lists[key] | |
433 self.untag(items) | |
434 | |
435 def untag(self, items): | |
436 """Untag some items. | |
437 | |
438 Check first if the items are not used in any panel. | |
439 | |
440 @param items (list): items to be removed | |
441 """ | |
442 items_assigned = set() | |
443 for values in self.getItemsByKey().itervalues(): | |
444 items_assigned.update(values) | |
445 quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned]) | |
446 | |
447 def getItemsByKey(self): | |
448 """Get the items grouped by list name. | |
449 | |
450 @return dict{unicode: list} | |
451 """ | |
452 return {key: self.lists[key]["panel"].getItems() for key in self.lists} | |
453 | |
454 def getKeysByItem(self): | |
455 """Get the keys groups by item. | |
456 | |
457 @return dict{object: set(unicode)} | |
458 """ | |
459 result = {} | |
460 for key in self.lists: | |
461 for item in self.lists[key]["panel"].getItems(): | |
462 result.setdefault(item, set()).add(key) | |
463 return result | |
464 | |
465 def registerPopupMenuPanel(self, entries, callback): | |
466 """Register a popup menu panel for the list names' buttons. | |
467 | |
468 @param entries (dict{unicode: dict{unicode: unicode}}): menu entries | |
469 @param callback (callable): common callback for all menu items, arguments are: | |
470 - button widget | |
471 - list name (item key) | |
472 """ | |
473 self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback) | |
474 for key in self.lists: # register click sender for already existing lists | |
475 self.popup_menu.registerClickSender(self.lists[key]["button"]) | |
476 | |
477 | |
478 class TagsPanel(base_panel.ToggleStackPanel): | |
479 """A toggle panel to set the tags""" | |
480 | |
481 TAGS = _("Tags") | |
482 | |
483 STYLE = {"main": "tagsPanel-main", | |
484 "tags": "tagsPanel-tags"} | |
485 | |
486 def __init__(self, suggested_tags, tags=None): | |
487 """ | |
488 | |
489 @param suggested_tags (list[unicode]): list of all suggested tags | |
490 @param tags (list[unicode]): already assigned tags | |
491 """ | |
492 base_panel.ToggleStackPanel.__init__(self, Width="100%") | |
493 self.addStyleName(self.STYLE["main"]) | |
494 | |
495 if tags is None: | |
496 tags = [] | |
497 | |
498 self.tags = ListPanel(quick_list_manager.QuickTagList(suggested_tags), tags) | |
499 self.tags.addStyleName(self.STYLE["tags"]) | |
500 self.tags.ACCEPT_NEW_ENTRY = True | |
501 self.add(self.tags, self.TAGS) | |
502 self.addStackChangeListener(self) | |
503 | |
504 def onStackChanged(self, sender, index, visible=None): | |
505 if visible is None: | |
506 visible = sender.getWidget(index).getVisible() | |
507 text = ", ".join(self.getTags()) | |
508 suffix = "" if (visible or not text) else (": %s" % text) | |
509 sender.setStackText(index, self.TAGS + suffix) | |
510 | |
511 def getTags(self): | |
512 return self.tags.getItems() | |
513 | |
514 def setTags(self, items): | |
515 self.tags.resetItems(items) | |
516 |