Mercurial > libervia-web
diff src/browser/list_manager.py @ 449:981ed669d3b3
/!\ reorganize all the file hierarchy, move the code and launching script to src:
- browser_side --> src/browser
- public --> src/browser_side/public
- libervia.py --> src/browser/libervia_main.py
- libervia_server --> src/server
- libervia_server/libervia.sh --> src/libervia.sh
- twisted --> src/twisted
- new module src/common
- split constants.py in 3 files:
- src/common/constants.py
- src/browser/constants.py
- src/server/constants.py
- output --> html (generated by pyjsbuild during the installation)
- new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css
- setup.py installs libervia to the following paths:
- src/common --> <LIB>/libervia/common
- src/server --> <LIB>/libervia/server
- src/twisted --> <LIB>/twisted
- html --> <SHARE>/libervia/html
- server_side --> <SHARE>libervia/server_side
- LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation:
- clean: remove previous installation directories
- purge: remove building and previous installation directories
You may need to update your sat.conf and/or launching script to update the following options/parameters:
- ssl_certificate
- data_dir
author | souliane <souliane@mailoo.org> |
---|---|
date | Tue, 20 May 2014 06:41:16 +0200 |
parents | browser_side/list_manager.py@d52f529a6d42 |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/browser/list_manager.py Tue May 20 06:41:16 2014 +0200 @@ -0,0 +1,608 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2013, 2014 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 pyjamas.ui.Grid import Grid +from pyjamas.ui.Button import Button +from pyjamas.ui.ListBox import ListBox +from pyjamas.ui.FlowPanel import FlowPanel +from pyjamas.ui.AutoComplete import AutoCompleteTextBox +from pyjamas.ui.Label import Label +from pyjamas.ui.HorizontalPanel import HorizontalPanel +from pyjamas.ui.VerticalPanel import VerticalPanel +from pyjamas.ui.DialogBox import DialogBox +from pyjamas.ui.KeyboardListener import KEY_ENTER +from pyjamas.ui.MouseListener import MouseHandler +from pyjamas.ui.FocusListener import FocusHandler +from pyjamas.ui.DropWidget import DropWidget +from pyjamas.Timer import Timer +from pyjamas import DOM + +from base_panels import PopupMenuPanel +from base_widget import DragLabel + +# HTML content for the removal button (image or text) +REMOVE_BUTTON = '<span class="recipientRemoveIcon">x</span>' + +# Item to be considered for an empty list box selection. +# Could be whatever which doesn't look like a JID or a group name. +EMPTY_SELECTION_ITEM = "" + + +class ListManager(): + """A manager for sub-panels to assign elements to lists.""" + + def __init__(self, parent, keys_dict={}, contact_list=[], offsets={}, style={}): + """ + @param parent: FlexTable parent widget for the manager + @param keys_dict: dict with the contact keys mapped to data + @param contact_list: list of string (the contact JID userhosts) + @param offsets: dict to set widget positions offset within parent + - "x_first": the x offset for the first widget's row on the grid + - "x": the x offset for all widgets rows, except the first one if "x_first" is defined + - "y": the y offset for all widgets columns on the grid + """ + self._parent = parent + if isinstance(keys_dict, set) or isinstance(keys_dict, list): + tmp = {} + for key in keys_dict: + tmp[key] = {} + keys_dict = tmp + self.__keys_dict = keys_dict + if isinstance(contact_list, set): + contact_list = list(contact_list) + self.__list = contact_list + self.__list.sort() + # store the list of contacts that are not assigned yet + self.__remaining_list = [] + self.__remaining_list.extend(self.__list) + # mark a change to sort the list before it's used + self.__remaining_list_sorted = True + + self.offsets = {"x_first": 0, "x": 0, "y": 0} + if "x" in offsets and not "x_first" in offsets: + offsets["x_first"] = offsets["x"] + self.offsets.update(offsets) + + self.style = { + "keyItem": "recipientTypeItem", + "popupMenuItem": "recipientTypeItem", + "buttonCell": "recipientButtonCell", + "dragoverPanel": "dragover-recipientPanel", + "keyPanel": "recipientPanel", + "textBox": "recipientTextBox", + "textBox-invalid": "recipientTextBox-invalid", + "removeButton": "recipientRemoveButton", + } + self.style.update(style) + + def createWidgets(self, title_format="%s"): + """Fill the parent grid with all the widgets (some may be hidden during the initialization).""" + self.__children = {} + for key in self.__keys_dict: + self.addContactKey(key, title_format=title_format) + + def addContactKey(self, key, dict_={}, title_format="%s"): + if key not in self.__keys_dict: + self.__keys_dict[key] = dict_ + # copy the key to its associated sub-map + self.__keys_dict[key]["title"] = key + self._addChild(self.__keys_dict[key], title_format) + + def removeContactKey(self, key): + """Remove a list panel and all its associated data.""" + contacts = self.__children[key]["panel"].getContacts() + (y, x) = self._parent.getIndex(self.__children[key]["button"]) + self._parent.removeRow(y) + del self.__children[key] + del self.__keys_dict[key] + self.addToRemainingList(contacts) + + def _addChild(self, entry, title_format): + """Add a button and FlowPanel for the corresponding map entry.""" + button = Button(title_format % entry["title"]) + button.setStyleName(self.style["keyItem"]) + if hasattr(entry, "desc"): + button.setTitle(entry["desc"]) + if not "optional" in entry: + entry["optional"] = False + button.setVisible(not entry["optional"]) + y = len(self.__children) + self.offsets["y"] + x = self.offsets["x_first"] if y == self.offsets["y"] else self.offsets["x"] + + self._parent.insertRow(y) + self._parent.setWidget(y, x, button) + self._parent.getCellFormatter().setStyleName(y, x, self.style["buttonCell"]) + + _child = ListPanel(self, entry, self.style) + self._parent.setWidget(y, x + 1, _child) + + self.__children[entry["title"]] = {} + self.__children[entry["title"]]["button"] = button + self.__children[entry["title"]]["panel"] = _child + + if hasattr(self, "popup_menu"): + # this is done if self.registerPopupMenuPanel has been called yet + self.popup_menu.registerClickSender(button) + + def _refresh(self, visible=True): + """Set visible the sub-panels that are non optional or non empty, hide the rest.""" + for key in self.__children: + self.setContactPanelVisible(key, False) + if not visible: + return + _map = self.getContacts() + for key in _map: + if len(_map[key]) > 0 or not self.__keys_dict[key]["optional"]: + self.setContactPanelVisible(key, True) + + def setVisible(self, visible): + self._refresh(visible) + + def setContactPanelVisible(self, key, visible=True, sender=None): + """Do not remove the "sender" param as it is needed for the context menu.""" + self.__children[key]["button"].setVisible(visible) + self.__children[key]["panel"].setVisible(visible) + + @property + def list(self): + """Return the full list of potential contacts.""" + return self.__list + + @property + def keys(self): + return self.__keys_dict.keys() + + @property + def keys_dict(self): + return self.__keys_dict + + @property + def remaining_list(self): + """Return the contacts that have not been selected yet.""" + if not self.__remaining_list_sorted: + self.__remaining_list_sorted = True + self.__remaining_list.sort() + return self.__remaining_list + + def setRemainingListUnsorted(self): + """Mark a change (deletion) so the list will be sorted before it's used.""" + self.__remaining_list_sorted = False + + def removeFromRemainingList(self, contacts): + """Remove contacts after they have been added to a sub-panel.""" + if not isinstance(contacts, list): + contacts = [contacts] + for contact_ in contacts: + if contact_ in self.__remaining_list: + self.__remaining_list.remove(contact_) + + def addToRemainingList(self, contacts, ignore_key=None): + """Add contacts after they have been removed from a sub-panel.""" + if not isinstance(contacts, list): + contacts = [contacts] + assigned_contacts = set() + assigned_map = self.getContacts() + for key_ in assigned_map.keys(): + if ignore_key is not None and key_ == ignore_key: + continue + assigned_contacts.update(assigned_map[key_]) + for contact_ in contacts: + if contact_ not in self.__list or contact_ in self.__remaining_list: + continue + if contact_ in assigned_contacts: + continue # the contact is assigned somewhere else + self.__remaining_list.append(contact_) + self.setRemainingListUnsorted() + + def setContacts(self, _map={}): + """Set the contacts for each contact key.""" + for key in self.__keys_dict: + if key in _map: + self.__children[key]["panel"].setContacts(_map[key]) + else: + self.__children[key]["panel"].setContacts([]) + self._refresh() + + def getContacts(self): + """Get the contacts for all the lists. + @return: a mapping between keys and contact lists.""" + _map = {} + for key in self.__children: + _map[key] = self.__children[key]["panel"].getContacts() + return _map + + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self._target_drop_cell + + def setTargetDropCell(self, target_drop_cell): + """@param: target_drop_cell: the panel where something has been dropped.""" + self._target_drop_cell = target_drop_cell + + def registerPopupMenuPanel(self, entries, hide, callback): + "Register a popup menu panel that will be bound to all contact keys elements." + self.popup_menu = PopupMenuPanel(entries=entries, hide=hide, callback=callback, style={"item": self.style["popupMenuItem"]}) + + +class DragAutoCompleteTextBox(AutoCompleteTextBox, DragLabel, MouseHandler, FocusHandler): + """A draggable AutoCompleteTextBox which is used for representing a contact. + This class is NOT generic because of the onDragEnd method which call methods + from ListPanel. It's probably not reusable for another scenario. + """ + + def __init__(self, parent, event_cbs, style): + AutoCompleteTextBox.__init__(self) + DragLabel.__init__(self, '', 'CONTACT_TEXTBOX') # The group prefix "@" is already in text so we use only the "CONTACT_TEXTBOX" type + self._parent = parent + self.event_cbs = event_cbs + self.style = style + self.addMouseListener(self) + self.addFocusListener(self) + self.addChangeListener(self) + self.addStyleName(style["textBox"]) + self.reset() + + def reset(self): + self.setText("") + self.setValid() + + def setValid(self, valid=True): + if self.getText() == "": + valid = True + if valid: + self.removeStyleName(self.style["textBox-invalid"]) + else: + self.addStyleName(self.style["textBox-invalid"]) + self.valid = valid + + def onDragStart(self, event): + self._text = self.getText() + DragLabel.onDragStart(self, event) + self._parent.setTargetDropCell(None) + self.setSelectionRange(len(self.getText()), 0) + + def onDragEnd(self, event): + target = self._parent.target_drop_cell # parent or another ListPanel + if self.getText() == "" or target is None: + return + self.event_cbs["drop"](self, target) + + def setRemoveButton(self): + + def remove_cb(sender): + """Callback for the button to remove this contact.""" + self._parent.remove(self) + self._parent.remove(self.remove_btn) + self.event_cbs["remove"](self) + + self.remove_btn = Button(REMOVE_BUTTON, remove_cb, Visible=False) + self.remove_btn.setStyleName(self.style["removeButton"]) + self._parent.add(self.remove_btn) + + def removeOrReset(self): + if hasattr(self, "remove_btn"): + self.remove_btn.click() + else: + self.reset() + + def onMouseMove(self, sender): + """Mouse enters the area of a DragAutoCompleteTextBox.""" + if hasattr(sender, "remove_btn"): + sender.remove_btn.setVisible(True) + + def onMouseLeave(self, sender): + """Mouse leaves the area of a DragAutoCompleteTextBox.""" + if hasattr(sender, "remove_btn"): + Timer(1500, lambda timer: sender.remove_btn.setVisible(False)) + + def onFocus(self, sender): + sender.setSelectionRange(0, len(self.getText())) + self.event_cbs["focus"](sender) + + def validate(self): + self.setSelectionRange(len(self.getText()), 0) + self.event_cbs["validate"](self) + + def onChange(self, sender): + """The textbox or list selection is changed""" + if isinstance(sender, ListBox): + AutoCompleteTextBox.onChange(self, sender) + self.validate() + + def onClick(self, sender): + """The list is clicked""" + AutoCompleteTextBox.onClick(self, sender) + self.validate() + + def onKeyUp(self, sender, keycode, modifiers): + """Listen for ENTER key stroke""" + AutoCompleteTextBox.onKeyUp(self, sender, keycode, modifiers) + if keycode == KEY_ENTER: + self.validate() + + +class DropCell(DropWidget): + """A cell where you can drop widgets. This class is NOT generic because of + onDrop which uses methods from ListPanel. It has been created to + separate the drag and drop methods from the others and add a bit of + lisibility, but it's probably not reusable for another scenario. + """ + + def __init__(self, drop_cbs): + DropWidget.__init__(self) + self.drop_cbs = drop_cbs + + def onDragEnter(self, event): + self.addStyleName(self.style["dragoverPanel"]) + DOM.eventPreventDefault(event) + + def onDragLeave(self, event): + if event.clientX <= self.getAbsoluteLeft() or event.clientY <= self.getAbsoluteTop()\ + or event.clientX >= self.getAbsoluteLeft() + self.getOffsetWidth() - 1\ + or event.clientY >= self.getAbsoluteTop() + self.getOffsetHeight() - 1: + # We check that we are inside widget's box, and we don't remove the style in this case because + # if the mouse is over a widget inside the DropWidget, we don't want the style to be removed + self.removeStyleName(self.style["dragoverPanel"]) + + def onDragOver(self, event): + DOM.eventPreventDefault(event) + + def onDrop(self, event): + DOM.eventPreventDefault(event) + dt = event.dataTransfer + # 'text', 'text/plain', and 'Text' are equivalent. + item, item_type = dt.getData("text/plain").split('\n') # Workaround for webkit, only text/plain seems to be managed + if item_type and item_type[-1] == '\0': # Workaround for what looks like a pyjamas bug: the \0 should not be there, and + item_type = item_type[:-1] # .strip('\0') and .replace('\0','') don't work. TODO: check this and fill a bug report + if item_type in self.drop_cbs.keys(): + self.drop_cbs[item_type](self, item) + self.removeStyleName(self.style["dragoverPanel"]) + + +VALID = 1 +INVALID = 2 +DELETE = 3 + + +class ListPanel(FlowPanel, DropCell): + """Sub-panel used for each contact key. Beware that pyjamas.ui.FlowPanel + is not fully implemented yet and can not be used with pyjamas.ui.Label.""" + + def __init__(self, parent, entry, style={}): + """Initialization with a button and a DragAutoCompleteTextBox.""" + FlowPanel.__init__(self, Visible=(False if entry["optional"] else True)) + drop_cbs = {"GROUP": lambda panel, item: self.addContact("@%s" % item), + "CONTACT": lambda panel, item: self.addContact(item), + "CONTACT_TITLE": lambda panel, item: self.addContact('@@'), + "CONTACT_TEXTBOX": lambda panel, item: self.setTargetDropCell(panel) + } + DropCell.__init__(self, drop_cbs) + self.style = style + self.addStyleName(self.style["keyPanel"]) + self._parent = parent + self.key = entry["title"] + self._addTextBox() + + def _addTextBox(self, switchPrevious=False): + """Add a text box to the last position. If switchPrevious is True, simulate + an insertion before the current last textbox by copying the text and valid state. + @return: the created textbox or the previous one if switchPrevious is True. + """ + if hasattr(self, "_last_textbox"): + if self._last_textbox.getText() == "": + return + self._last_textbox.setRemoveButton() + else: + switchPrevious = False + + def focus_cb(sender): + if sender != self._last_textbox: + # save the current value before it's being modified + self._parent.addToRemainingList(sender.getText(), ignore_key=self.key) + sender.setCompletionItems(self._parent.remaining_list) + + def remove_cb(sender): + """Callback for the button to remove this contact.""" + self._parent.addToRemainingList(sender.getText()) + self._parent.setRemainingListUnsorted() + self._last_textbox.setFocus(True) + + def drop_cb(sender, target): + """Callback when the textbox is drag-n-dropped.""" + parent = sender._parent + if target != parent and target.addContact(sender.getText()): + sender.removeOrReset() + else: + parent._parent.removeFromRemainingList(sender.getText()) + + events_cbs = {"focus": focus_cb, "validate": self.addContact, "remove": remove_cb, "drop": drop_cb} + textbox = DragAutoCompleteTextBox(self, events_cbs, self.style) + self.add(textbox) + if switchPrevious: + textbox.setText(self._last_textbox.getText()) + textbox.setValid(self._last_textbox.valid) + self._last_textbox.reset() + previous = self._last_textbox + self._last_textbox = textbox + return previous if switchPrevious else textbox + + def _checkContact(self, contact, modify): + """ + @param contact: the contact to check + @param modify: True if the contact is being modified + @return: + - VALID if the contact is valid + - INVALID if the contact is not valid but can be displayed + - DELETE if the contact should not be displayed at all + """ + def countItemInList(list_, item): + """For some reason the built-in count function doesn't work...""" + count = 0 + for elem in list_: + if elem == item: + count += 1 + return count + if contact is None or contact == "": + return DELETE + if countItemInList(self.getContacts(), contact) > (1 if modify else 0): + return DELETE + return VALID if contact in self._parent.list else INVALID + + def addContact(self, contact, sender=None): + """The first parameter type is checked, so it is also possible to call addContact(sender). + If contact is not defined, sender.getText() is used. If sender is not defined, contact will + be written to the last textbox and a new textbox is added afterward. + @param contact: unicode + @param sender: DragAutoCompleteTextBox instance + """ + if isinstance(contact, DragAutoCompleteTextBox): + sender = contact + contact = sender.getText() + valid = self._checkContact(contact, sender is not None) + if sender is None: + # method has been called to modify but to add a contact + if valid == VALID: + # eventually insert before the last textbox if it's not empty + sender = self._addTextBox(True) if self._last_textbox.getText() != "" else self._last_textbox + sender.setText(contact) + else: + sender.setValid(valid == VALID) + if valid != VALID: + if sender is not None and valid == DELETE: + sender.removeOrReset() + return False + if sender == self._last_textbox: + self._addTextBox() + try: + sender.setVisibleLength(len(contact)) + except: + # IndexSizeError: Index or size is negative or greater than the allowed amount + log.warning("FIXME: len(%s) returns %d... javascript bug?" % (contact, len(contact))) + self._parent.removeFromRemainingList(contact) + self._last_textbox.setFocus(True) + return True + + def emptyContacts(self): + """Empty the list of contacts.""" + for child in self.getChildren(): + if hasattr(child, "remove_btn"): + child.remove_btn.click() + + def setContacts(self, tab): + """Set the contacts.""" + self.emptyContacts() + if isinstance(tab, set): + tab = list(tab) + tab.sort() + for contact in tab: + self.addContact(contact) + + def getContacts(self): + """Get the contacts + @return: an array of string""" + tab = [] + for widget in self.getChildren(): + if isinstance(widget, DragAutoCompleteTextBox): + # not to be mixed with EMPTY_SELECTION_ITEM + if widget.getText() != "": + tab.append(widget.getText()) + return tab + + @property + def target_drop_cell(self): + """@return: the panel where something has been dropped.""" + return self._parent.target_drop_cell + + def setTargetDropCell(self, target_drop_cell): + """ + XXX: Property setter here would not make it, you need a proper method! + @param target_drop_cell: the panel where something has been dropped.""" + self._parent.setTargetDropCell(target_drop_cell) + + +class ContactChooserPanel(DialogBox): + """Display the contacts chooser dialog. This has been implemented while + prototyping and is currently not used. Left for an eventual later use. + Replaced by the popup menu which allows to add a panel for Cc or Bcc. + """ + + def __init__(self, manager, **kwargs): + """Display a listbox for each contact key""" + DialogBox.__init__(self, autoHide=False, centered=True, **kwargs) + self.setHTML("Select contacts") + self.manager = manager + self.listboxes = {} + self.contacts = manager.getContacts() + + container = VerticalPanel(Visible=True) + container.addStyleName("marginAuto") + + grid = Grid(2, len(self.manager.keys_dict)) + index = -1 + for key in self.manager.keys_dict: + index += 1 + grid.add(Label("%s:" % self.manager.keys_dict[key]["desc"]), 0, index) + listbox = ListBox() + listbox.setMultipleSelect(True) + listbox.setVisibleItemCount(15) + listbox.addItem(EMPTY_SELECTION_ITEM) + for element in manager.list: + listbox.addItem(element) + self.listboxes[key] = listbox + grid.add(listbox, 1, index) + self._reset() + + buttons = HorizontalPanel() + buttons.addStyleName("marginAuto") + btn_close = Button("Cancel", self.hide) + buttons.add(btn_close) + btn_reset = Button("Reset", self._reset) + buttons.add(btn_reset) + btn_ok = Button("OK", self._validate) + buttons.add(btn_ok) + + container.add(grid) + container.add(buttons) + + self.add(container) + self.center() + + def _reset(self): + """Reset the selections.""" + for key in self.manager.keys_dict: + listbox = self.listboxes[key] + for i in xrange(0, listbox.getItemCount()): + if listbox.getItemText(i) in self.contacts[key]: + listbox.setItemSelected(i, "selected") + else: + listbox.setItemSelected(i, "") + + def _validate(self): + """Sets back the selected contacts to the good sub-panels.""" + _map = {} + for key in self.manager.keys_dict: + selections = self.listboxes[key].getSelectedItemText() + if EMPTY_SELECTION_ITEM in selections: + selections.remove(EMPTY_SELECTION_ITEM) + _map[key] = selections + self.manager.setContacts(_map) + self.hide()