Mercurial > libervia-web
view src/browser/sat_browser/list_manager.py @ 907:3d372805f60c 0.6.1
installation (setup.py): changed version to 0.6.1.1 so new archive with fixed MANIFEST can be uploaded on pypi
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 13 Jul 2016 22:45:44 +0200 |
parents | f8a7a046ff9c |
children |
line wrap: on
line source
#!/usr/bin/python # -*- coding: utf-8 -*- # Libervia: a Salut à Toi frontend # Copyright (C) 2013-2016 Adrien Cossa <souliane@mailoo.org> # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.log import getLogger log = getLogger(__name__) from sat.core.i18n import _ from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui.FocusListener import FocusHandler from pyjamas.ui.ChangeListener import ChangeHandler from pyjamas.ui.DragHandler import DragHandler from pyjamas.ui.KeyboardListener import KeyboardHandler, KEY_ENTER from pyjamas.ui.DragWidget import DragWidget from pyjamas.ui.ListBox import ListBox from pyjamas.ui.Button import Button from pyjamas.ui.FlowPanel import FlowPanel from pyjamas.ui.HorizontalPanel import HorizontalPanel from pyjamas.ui.FlexTable import FlexTable from pyjamas.ui.AutoComplete import AutoCompleteTextBox import base_panel import base_widget import libervia_widget from sat_frontends.quick_frontend import quick_list_manager unicode = str # FIXME: pyjamas workaround class ListItem(HorizontalPanel): """This class implements a list item with auto-completion and a delete button.""" STYLE = {"listItem": "listItem", "listItem-box": "listItem-box", "listItem-box-invalid": "listItem-box-invalid", "listItem-button": "listItem-button", } VALID = 1 INVALID = 2 DUPLICATE = 3 def __init__(self, listener=None, taglist=None, validate=None): """ @param listener (ListItemHandler): handler for the UI events @param taglist (quick_list_manager.QuickTagList): list manager @param validate (callable): method returning a bool to validate the entry """ HorizontalPanel.__init__(self) self.addStyleName(self.STYLE["listItem"]) self.box = AutoCompleteTextBox(StyleName=self.STYLE["listItem-box"]) self.remove_btn = Button('<span>x</span>', Visible=False) self.remove_btn.setStyleName(self.STYLE["listItem-button"]) self.add(self.box) self.add(self.remove_btn) if listener: self.box.addFocusListener(listener) self.box.addChangeListener(listener) self.box.addKeyboardListener(listener) self.box.choices.addClickListener(listener) self.remove_btn.addClickListener(listener) self.taglist = taglist self.validate = validate self.last_checked_value = "" self.last_validity = self.VALID @property def text(self): return self.box.getText() def setText(self, text): """ Set the text and refresh the Widget. @param text (unicode): text to set """ self.box.setText(text) self.refresh() def refresh(self): if self.last_checked_value == self.text: return if self.taglist and self.last_checked_value: self.taglist.untag([self.last_checked_value]) if self.validate: # if None, the state is always valid self.last_validity = self.validate(self.text) if self.last_validity == self.VALID: self.box.removeStyleName(self.STYLE["listItem-box-invalid"]) self.box.setVisibleLength(max(len(self.text), 10)) elif self.last_validity == self.INVALID: self.box.addStyleName(self.STYLE["listItem-box-invalid"]) elif self.last_validity == self.DUPLICATE: self.remove_btn.click() # this may do more stuff then self.remove() return if self.taglist and self.text: self.taglist.tag([self.text]) self.last_checked_value = self.text self.box.setSelectionRange(len(self.text), 0) self.remove_btn.setVisible(len(self.text) > 0) def setFocus(self, focused): self.box.setFocus(focused) def remove(self): """Remove the list item from its parent.""" self.removeFromParent() if self.taglist and self.text: # this must be done after the widget has been removed self.taglist.untag([self.text]) class DraggableListItem(ListItem, DragWidget): """This class is like ListItem, but in addition it can be dragged.""" def __init__(self, listener=None, taglist=None, validate=None): """ @param listener (ListItemHandler): handler for the UI events @param taglist (quick_list_manager.QuickTagList): list manager @param validate (callable): method returning a bool to validate the entry """ ListItem.__init__(self, listener, taglist, validate) DragWidget.__init__(self) self.addDragListener(listener) def onDragStart(self, event): """The user starts dragging the item.""" dt = event.dataTransfer dt.setData('text/plain', "%s\n%s" % (self.text, "CONTACT_TEXTBOX")) dt.setDragImage(self.box.getElement(), 15, 15) class ListItemHandler(ClickHandler, FocusHandler, KeyboardHandler, ChangeHandler): """Implements basic handlers for the ListItem events.""" last_item = None # the last item is an empty text box for user input def __init__(self, taglist): """ @param taglist (quick_list_manager.QuickTagList): list manager """ ClickHandler.__init__(self) FocusHandler.__init__(self) ChangeHandler.__init__(self) KeyboardHandler.__init__(self) self.taglist = taglist def addItem(self, item): raise NotImplementedError def removeItem(self, item): raise NotImplementedError def onClick(self, sender): """The remove button or a suggested completion item has been clicked.""" #log.debug("onClick sender type: %s" % type(sender)) if isinstance(sender, Button): item = sender.getParent() self.removeItem(item) elif isinstance(sender, ListBox): # this is called after onChange when you click a suggested item, and now we get the final value textbox = sender._clickListeners[0] self.checkValue(textbox) else: raise AssertionError def onFocus(self, sender): """The text box has the focus.""" #log.debug("onFocus sender type: %s" % type(sender)) assert isinstance(sender, AutoCompleteTextBox) sender.setCompletionItems(self.taglist.untagged) def onKeyUp(self, sender, keycode, modifiers): """The text box is being modified - or ENTER key has been pressed.""" # this is called after onChange when you press ENTER, and now we get the final value #log.debug("onKeyUp sender type: %s" % type(sender)) assert isinstance(sender, AutoCompleteTextBox) if keycode == KEY_ENTER: self.checkValue(sender) def onChange(self, sender): """The text box has been changed by the user.""" # this is called before the completion when you press ENTER or click a suggest item #log.debug("onChange sender type: %s" % type(sender)) assert isinstance(sender, AutoCompleteTextBox) self.checkValue(sender) def checkValue(self, textbox): """Internal handler to call when a new value is submitted by the user.""" item = textbox.getParent() if item.text == item.last_checked_value: # this method has already been called (by self.onChange) and there's nothing new return item.refresh() if item == self.last_item and item.last_validity == ListItem.VALID and item.text: self.addItem() class DraggableListItemHandler(ListItemHandler, DragHandler): """Implements basic handlers for the DraggableListItem events.""" def __init__(self, manager): """ @param manager (ListManager): list manager """ ListItemHandler.__init__(self, manager) DragHandler.__init__(self) @property def manager(self): return self.taglist def onDragStart(self, event): """The user starts dragging the item.""" self.manager.drop_target = None def onDragEnd(self, event): """The user dropped the list item.""" text, dummy = libervia_widget.eventGetData(event) target = self.manager.drop_target # self or another ListPanel if text == "" or target is None: return if target != self: # move the item from self to target target.addItem(text) self.removeItem(self.getItem(text)) class ListPanel(FlowPanel, DraggableListItemHandler, libervia_widget.DropCell): """Implements a list of items.""" # XXX: beware that pyjamas.ui.FlowPanel is not fully implemented: # - it can not be used with pyjamas.ui.Label # - FlowPanel.insert doesn't work STYLE = {"listPanel": "listPanel"} ACCEPT_NEW_ENTRY = False def __init__(self, manager, items=None): """Initialization with a button for the list name (key) and a DraggableListItem. @param manager (ListManager): list manager @param items (list): items to be set """ FlowPanel.__init__(self) DraggableListItemHandler.__init__(self, manager) libervia_widget.DropCell.__init__(self, None) self.addStyleName(self.STYLE["listPanel"]) self.manager = manager self.resetItems(items) # FIXME: dirty magic strings '@' and '@@' self.drop_keys = {"GROUP": lambda host, item_s: self.addItem("@%s" % item_s), "CONTACT": lambda host, item_s: self.addItem(item_s), "CONTACT_TITLE": lambda host, item_s: self.addItem('@@'), "CONTACT_TEXTBOX": lambda host, item_s: setattr(self.manager, "drop_target", self), } def onDrop(self, event): """Something has been dropped in this ListPanel""" try: libervia_widget.DropCell.onDrop(self, event) except base_widget.NoLiberviaWidgetException: pass def getItem(self, text): """Get an item from its text. @param text(unicode): item text """ for child in self.getChildren(): if child.text == text: return child return None def getItems(self): """Get the non empty items. @return list(unicode) """ return [widget.text for widget in self.getChildren() if isinstance(widget, ListItem) and widget.text] def validateItem(self, text): """Return validation code after the item has been changed. @param text (unicode): item text to check @return: int value defined by one of these constants: - VALID if the item is valid - INVALID if the item is not valid but can be displayed - DUPLICATE if the item is a duplicate """ def count(list_, item): # XXX: list.count in not implemented by pyjamas return len([elt for elt in list_ if elt == item]) if count(self.getItems(), text) > 1: return ListItem.DUPLICATE # item already exists in this list so we suggest its deletion if self.ACCEPT_NEW_ENTRY: return ListItem.VALID return ListItem.VALID if text in self.manager.items or not text else ListItem.INVALID def addItem(self, text=""): """Add an item. @param text (unicode): text to be set. @return: True if the item has been really added or merged. """ if text in self.getItems(): # avoid duplicate in the same list return item = DraggableListItem(self, self.manager, self.validateItem) self.add(item) if self.last_item: if self.last_item.last_validity == ListItem.INVALID: # switch the two values so that the invalid one stays in last position item.setText(self.last_item.text) self.last_item.setText(text) elif not self.last_item.text: # copy the new value to previous empty item self.last_item.setText(text) else: # first item of the list, or previous last item has been deleted item.setText(text) self.last_item = item self.last_item.setFocus(True) def removeItem(self, item): """Remove an item. @param item(DraggableListItem): item to remove """ if item == self.last_item: self.addItem("") item.remove() # this also updates the taglist def resetItems(self, items): """Reset the items. @param items (list): items to be set """ for child in self.getChildren(): child.remove() self.addItem() if not items: return items.sort() for item in items: self.addItem(unicode(item)) class ListManager(FlexTable, quick_list_manager.QuickTagList): """Implements a table to manage one or several lists of items.""" STYLE = {"listManager-button": "group", "listManager-button-cell": "listManager-button-cell", } def __init__(self, data=None, items=None): """ @param data (dict{unicode: list}): dict binding keys to tagged items. @param items (list): full list of items (tagged and untagged) """ FlexTable.__init__(self, Width="100%") quick_list_manager.QuickTagList.__init__(self, [unicode(item) for item in items]) self.lists = {} if data: for key, items in data.iteritems(): self.addList(key, [unicode(item) for item in items]) def addList(self, key, items=None): """Add a Button and ListPanel for a new list. @param key (unicode): list name @param items (list): items to append to the new list """ if key in self.lists: return if items is None: items = [] self.lists[key] = {"button": Button(key, Title=key, StyleName=self.STYLE["listManager-button"]), "panel": ListPanel(self, items)} y, x = len(self.lists), 0 self.insertRow(y) self.setWidget(y, x, self.lists[key]["button"]) self.setWidget(y, x + 1, self.lists[key]["panel"]) self.getCellFormatter().setStyleName(y, x, self.STYLE["listManager-button-cell"]) try: self.popup_menu.registerClickSender(self.lists[key]["button"]) except (AttributeError, TypeError): # self.registerPopupMenuPanel hasn't been called yet pass def removeList(self, key): """Remove a ListPanel from this manager. @param key (unicode): list name """ items = self.lists[key]["panel"].getItems() (y, x) = self.getIndex(self.lists[key]["button"]) self.removeRow(y) del self.lists[key] self.untag(items) def untag(self, items): """Untag some items. Check first if the items are not used in any panel. @param items (list): items to be removed """ items_assigned = set() for values in self.getItemsByKey().itervalues(): items_assigned.update(values) quick_list_manager.QuickTagList.untag(self, [item for item in items if item not in items_assigned]) def getItemsByKey(self): """Get the items grouped by list name. @return dict{unicode: list} """ return {key: self.lists[key]["panel"].getItems() for key in self.lists} def getKeysByItem(self): """Get the keys groups by item. @return dict{object: set(unicode)} """ result = {} for key in self.lists: for item in self.lists[key]["panel"].getItems(): result.setdefault(item, set()).add(key) return result def registerPopupMenuPanel(self, entries, callback): """Register a popup menu panel for the list names' buttons. @param entries (dict{unicode: dict{unicode: unicode}}): menu entries @param callback (callable): common callback for all menu items, arguments are: - button widget - list name (item key) """ self.popup_menu = base_panel.PopupMenuPanel(entries, callback=callback) for key in self.lists: # register click sender for already existing lists self.popup_menu.registerClickSender(self.lists[key]["button"]) class TagsPanel(base_panel.ToggleStackPanel): """A toggle panel to set the tags""" TAGS = _("Tags") STYLE = {"main": "tagsPanel-main", "tags": "tagsPanel-tags"} def __init__(self, suggested_tags, tags=None): """ @param suggested_tags (list[unicode]): list of all suggested tags @param tags (list[unicode]): already assigned tags """ base_panel.ToggleStackPanel.__init__(self, Width="100%") self.addStyleName(self.STYLE["main"]) if tags is None: tags = [] self.tags = ListPanel(quick_list_manager.QuickTagList(suggested_tags), tags) self.tags.addStyleName(self.STYLE["tags"]) self.tags.ACCEPT_NEW_ENTRY = True self.add(self.tags, self.TAGS) self.addStackChangeListener(self) def onStackChanged(self, sender, index, visible=None): if visible is None: visible = sender.getWidget(index).getVisible() text = ", ".join(self.getTags()) suffix = "" if (visible or not text) else (": %s" % text) sender.setStackText(index, self.TAGS + suffix) def getTags(self): return self.tags.getItems() def setTags(self, items): self.tags.resetItems(items)