changeset 241:86055ccf69c3

browser_side: added class PopupMenuPanel to manage more complex context menu
author souliane <souliane@mailoo.org>
date Tue, 15 Oct 2013 13:36:51 +0200
parents a565ce2facc0
children a25aa882e09a
files browser_side/panels.py browser_side/recipients.py public/libervia.css
diffstat 3 files changed, 134 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- 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)
--- 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)
--- 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;
+}