# HG changeset patch # User souliane # Date 1381837011 -7200 # Node ID 86055ccf69c3d2a7950a569802e6df08b8dedc42 # Parent a565ce2facc013515b05974c063b6d57b698e733 browser_side: added class PopupMenuPanel to manage more complex context menu diff -r a565ce2facc0 -r 86055ccf69c3 browser_side/panels.py --- a/browser_side/panels.py Tue Oct 15 13:39:21 2013 +0200 +++ b/browser_side/panels.py Tue Oct 15 13:36:51 2013 +0200 @@ -32,8 +32,10 @@ from pyjamas.ui.Button import Button from pyjamas.ui.HTML import HTML from pyjamas.ui.Image import Image +from pyjamas.ui.PopupPanel import PopupPanel from pyjamas.ui.ClickListener import ClickHandler from pyjamas.ui.KeyboardListener import KEY_ENTER, KEY_UP, KEY_DOWN +from pyjamas.ui.Event import BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT from pyjamas.ui.MouseListener import MouseHandler from pyjamas.Timer import Timer from pyjamas import DOM @@ -546,9 +548,9 @@ entry.addStyleName('selected_entry') self.selected_entry = entry - def updateValue(self, type, jid, value): + def updateValue(self, type_, jid, value): """Update a jid value in entries - @param type: one of 'avatar', 'nick' + @param type_: one of 'avatar', 'nick' @param jid: jid concerned @param value: new value""" def updateVPanel(vpanel): @@ -557,7 +559,7 @@ child.updateAvatar(value) elif isinstance(child, VerticalPanel): updateVPanel(child) - if type == 'avatar': + if type_ == 'avatar': updateVPanel(self.vpanel) def setAcceptedGroup(self, group): @@ -940,3 +942,108 @@ tab_bar_h = _elts.item(0).offsetHeight ideal_height = Window.getClientHeight() - tab_bar_h self.setHeight("%s%s" % (ideal_height, "px")) + + +class PopupMenuPanel(PopupPanel): + """This implementation of a popup menu (context menu) allow you to assign + two special methods which are common to all the items, in order to hide + certain items and also easily define their callbacks. The menu can be + bound to any of the mouse button (left, middle, right). + """ + def __init__(self, entries, hide=None, callback=None, vertical=True, item_style="popupMenuItem", menu_style=None, **kwargs): + """ + @param entries: a dict of dicts, where each sub-dict is representing + one menu item: the sub-dict key can be used as the item text and + description, but optional "title" and "desc" entries would be used + if they exists. The sub-dicts may be extended later to do + more complicated stuff or overwrite the common methods. + @param hide: function with 2 args: widget, key as string and + returns True if that item should be hidden from the context menu. + @param callback: function with 2 args: widget, key as string + @param vertical: True or False, to set the direction + @param item_style: alternative CSS class for the menu items + @param menu_style: supplementary CSS class for the sender widget + """ + PopupPanel.__init__(self, autoHide=True, **kwargs) + self._entries = entries + self._hide = hide + self._callback = callback + self.vertical = vertical + self.item_style = item_style + self.menu_style = menu_style + self._senders = {} + + def _show(self, sender): + """Popup the menu relative to this sender's position. + @param sender: the widget that has been clicked + """ + menu = VerticalPanel() if self.vertical is True else HorizontalPanel() + menu.setStyleName("recipientTypeMenu") + + def button_cb(item): + """You can not put that method in the loop and rely + on _key, because it is overwritten by each step. + You can rely on item.key instead, which is copied + from _key after the item creation. + @param item: the menu item that has been clicked + """ + if self._callback is not None: + self._callback(sender=sender, key=item.key) + self.hide(autoClosed=True) + + for _key in self._entries.keys(): + entry = self._entries[_key] + if self._hide is not None and self._hide(sender=sender, key=_key) is True: + continue + title = entry["title"] if "title" in entry.keys() else _key + item = Button(title, button_cb) + item.key = _key + item.setStyleName(self.item_style) + item.setTitle(entry["desc"] if "desc" in entry.keys() else title) + menu.add(item) + self.add(menu) + if self.vertical is True: + x = sender.getAbsoluteLeft() + sender.getOffsetWidth() + y = sender.getAbsoluteTop() + else: + x = sender.getAbsoluteLeft() + y = sender.getAbsoluteTop() + sender.getOffsetHeight() + self.setPopupPosition(x, y) + self.show() + if self.menu_style: + sender.addStyleDependentName(self.menu_style) + + def _onHide(popup): + if hasattr(self, "menu_style") and self.menu_style is not None: + sender.removeStyleDependentName(self.menu_style) + return PopupPanel.onHideImpl(self, popup) + + self.onHideImpl = _onHide + + def registerClickSender(self, sender, button=BUTTON_LEFT): + """Bind the menu to the specified sender. + @param sender: the widget to which the menu should be bound + @param: BUTTON_LEFT, BUTTON_MIDDLE or BUTTON_RIGHT + """ + self._senders.setdefault(sender, []) + self._senders[sender].append(button) + + if button == BUTTON_RIGHT: + # WARNING: to disable the context menu is a bit tricky... + # The following seems to work on Firefox 24.0, but: + # TODO: find a cleaner way to disable the context menu + sender.getElement().setAttribute("oncontextmenu", "return false") + + def _onBrowserEvent(event): + button = DOM.eventGetButton(event) + if DOM.eventGetType(event) == "mousedown" and button in self._senders[sender]: + self._show(sender) + return sender.__class__.onBrowserEvent(sender, event) + + sender.onBrowserEvent = _onBrowserEvent + + def registerMiddleClickSender(self, sender): + self.registerClickSender(sender, BUTTON_MIDDLE) + + def registerRightClickSender(self, sender): + self.registerClickSender(sender, BUTTON_RIGHT) diff -r a565ce2facc0 -r 86055ccf69c3 browser_side/recipients.py --- a/browser_side/recipients.py Tue Oct 15 13:39:21 2013 +0200 +++ b/browser_side/recipients.py Tue Oct 15 13:36:51 2013 +0200 @@ -23,7 +23,6 @@ from pyjamas.ui.Button import Button from pyjamas.ui.ListBox import ListBox from pyjamas.ui.FlowPanel import FlowPanel -from pyjamas.ui.PopupPanel import PopupPanel from pyjamas.ui.AutoComplete import AutoCompleteTextBox from pyjamas.ui.Label import Label from pyjamas.ui.HorizontalPanel import HorizontalPanel @@ -36,10 +35,11 @@ from pyjamas.ui.DragWidget import DragWidget from pyjamas.Timer import Timer from pyjamas import DOM +import panels # Map the recipient types to their properties. For convenience, the key # value is copied during the initialization to its associated sub-map, -# stored in the value of a new entry which uses "key" as its key. +# stored in the value of a new entry which uses "title" as its key. RECIPIENT_TYPES = {"To": {"desc": "Direct recipients", "optional": False}, "Cc": {"desc": "Carbon copies", "optional": True}, "Bcc": {"desc": "Blind carbon copies", "optional": True}} @@ -72,18 +72,24 @@ # mark a change to sort the list before it's used self.__remaining_list_sorted = True + self.recipient_menu = panels.PopupMenuPanel(entries=RECIPIENT_TYPES, + hide=lambda sender, key: self.__children[key]["panel"].isVisible(), + callback=self.setRecipientPanelVisible, + item_style="recipientTypeItem") + def createWidgets(self): """Fill the parent grid with all the widgets but only show those for non optional recipient types.""" self.__children = {} for key in RECIPIENT_TYPES: # copy the key to its associated sub-map - RECIPIENT_TYPES[key]["key"] = key + RECIPIENT_TYPES[key]["title"] = key self._addChild(RECIPIENT_TYPES[key]) def _addChild(self, entry): """Add a button and FlowPanel for the corresponding map entry.""" - button = Button("%s: " % entry["key"], self.selectRecipientType) + button = Button("%s: " % entry["title"]) + self.recipient_menu.registerClickSender(button) button.addStyleName("recipientTypeItem") button.setTitle(entry["desc"]) button.setVisible(not entry["optional"]) @@ -93,9 +99,9 @@ _child = RecipientTypePanel(self, entry) self._parent.setWidget(len(self.__children), 1, _child) - self.__children[entry["key"]] = {} - self.__children[entry["key"]]["button"] = button - self.__children[entry["key"]]["panel"] = _child + self.__children[entry["title"]] = {} + self.__children[entry["title"]]["button"] = button + self.__children[entry["title"]]["panel"] = _child def _refresh(self): """Set visible the sub-panels that are non optional or non empty, hide the rest.""" @@ -106,7 +112,8 @@ if len(_map[key]) > 0 or not RECIPIENT_TYPES[key]["optional"]: self.setRecipientPanelVisible(key, True) - def setRecipientPanelVisible(self, key, visible=True): + def setRecipientPanelVisible(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) @@ -138,33 +145,10 @@ def selectRecipients(self): """Display the recipients chooser dialog. This has been implemented while prototyping and is currently not used. Left for an eventual later use. - Replaced by self.selectRecipientType. + Replaced by the popup menu which allows to add a panel for Cc or Bcc. """ RecipientChooserPanel(self) - def selectRecipientType(self, sender): - """Display a context menu to add a new recipient type.""" - self.context_menu = VerticalPanel() - self.context_menu.setStyleName("recipientTypeMenu") - popup = PopupPanel(autoHide=True) - - for key in RECIPIENT_TYPES: - if self.__children[key]["panel"].isVisible(): - continue - - def showPanel(sender): - self.setRecipientPanelVisible(sender.getText()) - popup.hide(autoClosed=True) - - item = Button(key, showPanel) - item.setStyleName("recipientTypeItem") - item.setTitle(RECIPIENT_TYPES[key]["desc"]) - self.context_menu.add(item) - - popup.add(self.context_menu) - popup.setPopupPosition(sender.getAbsoluteLeft() + sender.getOffsetWidth(), sender.getAbsoluteTop()) - popup.show() - def setRecipients(self, _map={}): """Set the recipients for each recipient types.""" for key in RECIPIENT_TYPES: @@ -358,10 +342,10 @@ textbox.setText(recipient) self.add(textbox) try: - textbox.setVisibleLength(len(recipient)) + textbox.setVisibleLength(len(str(recipient))) except: #TODO: . how come could this happen?! len(recipient) is sometimes 0 but recipient is not empty - print "len(recipient) returns %d where recipient == %s..." % (len(recipient), recipient) + print "len(recipient) returns %d where recipient == %s..." % (len(str(recipient)), str(recipient)) self._parent.removeFromRemainingList(recipient) remove_btn = Button(REMOVE_BUTTON, Visible=False) diff -r a565ce2facc0 -r 86055ccf69c3 public/libervia.css --- a/public/libervia.css Tue Oct 15 13:39:21 2013 +0200 +++ b/public/libervia.css Tue Oct 15 13:36:51 2013 +0200 @@ -1243,3 +1243,9 @@ border: 1px dashed rgb(35,79,255); } +/* Popup (context) menu */ + +.popupMenuItem { + cursor: pointer; + border-radius: 5px; +}