view src/browser/sat_browser/base_menu.py @ 498:60be99de3808

browser_side: menus refactorization + handle levels > 2
author souliane <souliane@mailoo.org>
date Fri, 25 Jul 2014 02:38:30 +0200
parents 5d8632a7bfde
children 4aa627b059df
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.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/>.


"""Base classes for building a menu.

These classes have been moved here from menu.py because they are also used
by base_widget.py, and the import sequence caused a JS runtime error."""


import pyjd  # this is dummy in pyjs
from sat.core.log import getLogger
log = getLogger(__name__)

from pyjamas.ui.MenuBar import MenuBar
from pyjamas.ui.MenuItem import MenuItem
from pyjamas import Window


class MenuCmd:
    """Return an object with an "execute" method that can be set to a menu item callback"""

    def __init__(self, object_, handler):
        self._object = object_
        self._handler = handler

    def execute(self):
        handler = getattr(self._object, self._handler)
        handler()


class PluginMenuCmd:
    """Like MenuCmd, but instead of executing a method, it will command the bridge to launch an action"""

    def __init__(self, host, action_id, menu_data=None):
        self.host = host
        self.action_id = action_id
        self.menu_data = menu_data

    def execute(self):
        self.host.launchAction(self.action_id, self.menu_data)


class CategoryItem(MenuItem):
    """A category item with a non-internationalized name"""
    def __init__(self, name, *args, **kwargs):
        MenuItem.__init__(self, *args, **kwargs)
        self.name = name


class GenericMenuBar(MenuBar):
    """A menu bar with sub-categories and items"""

    def __init__(self, host, vertical=False, styles=None, **kwargs):
        """
        @param host (SatWebFrontend): host instance
        @param vertical (bool): True to display the popup menu vertically
        @param styles (dict): specific styles to be applied:
            - key: a value in ('moved_popup', 'menu_bar')
            - value: a CSS class
        the popup that are not displayed at the position computed by pyjamas.
        """
        MenuBar.__init__(self, vertical, **kwargs)
        self.host = host
        self.styles = styles or {}
        if 'menu_bar' in self.styles:
            # XXX: pyjamas set the style to object string representation!
            # FIXME: fix the bug upstream
            first = 'gwt-MenuBar'
            second = first + '-' + ('vertical' if self.vertical else 'horizontal')
            self.setStyleName(' '.join([first, second, self.styles['menu_bar']]))

    @classmethod
    def getCategoryHTML(cls, type_, menu_name_i18n):
        """Build the html to be used for displaying a category item.

        Inheriting classes may overwrite this method.
        @param type_ (str): category type
        @param menu_name_i18n (str): internationalized category name
        @return: str
        """
        return menu_name_i18n

    def doItemAction(self, item, fireCommand):
        """Overwrites the default behavior for the popup menu to fit in the screen"""
        MenuBar.doItemAction(self, item, fireCommand)
        if not self.popup:
            return
        if self.vertical:
            # move the popup if it would go over the screen's viewport
            max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth()
            new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1
            top = item.getAbsoluteTop()
        else:
            # move the popup if it would go over the menu bar right extremity
            max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth()
            new_left = max_left
            top = self.getAbsoluteTop() + self.getOffsetHeight() - 1
        if item.getAbsoluteLeft() > max_left:
            self.popup.setPopupPosition(new_left, top)
            # eventually smooth the popup edges to fit the menu own style
            if 'moved_popup' in self.styles:
                self.popup.addStyleName(self.styles['moved_popup'])

    def getCategories(self):
        """Return all the categories items.

        @return: list[CategoryItem]
        """
        return [item for item in self.items if isinstance(item, CategoryItem)]

    def getCategoryItem(self, path):
        """Return the requested category item

        @param path (list[str]): path to the category
        @return: CategoryInstance or None
        """
        assert(len(path) > 0)
        if len(path) > 1:
            menu = self.getCategoryMenu(path[:1])
            return menu.getCategoryItem(path[1:]) if menu else None
        items = [item for item in self.items if isinstance(item, CategoryItem) and item.name == path[0]]
        if len(items) == 1:
            return items[0]
        assert(items == [])  # there should not be more than 1 category with the same name
        return None

    def getCategoryMenu(self, path):
        """Return the popup menu for the given category

        @param path (list[str]): path to the category
        @return: CategoryMenuBar instance or None
        """
        item = self.getCategoryItem(path)
        return item.getSubMenu() if item else None

    def addSeparator(self):
        """Add a separator between the categories"""
        self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator'))

    def addCategory(self, path, path_i18n, type_, sub_menu=None):
        """Add a category item and its associated sub-menu.

        If the category already exists, do not overwrite the current sub-menu.
        @param path (list[str], str): path to the category. Passing a string for
        the category name is also accepted if there's no sub-category.
        @param path_i18n (list[str], str): internationalized path to the category.
        Passing a string for the internationalized category name is also accepted
        if there's no sub-category.
        @param type_ (str): category type
        @param sub_menu (CategoryMenuBar): category sub-menu
        """
        if isinstance(path, str):
            path = [path]
        if isinstance(path_i18n, str):
            path_i18n = [path_i18n]
        assert(len(path) > 0 and len(path) == len(path_i18n))
        current = self
        count = len(path)
        for menu_name, menu_name_i18n in zip(path, path_i18n):
            tmp = current.getCategoryMenu([menu_name])
            if not tmp:
                html = self.getCategoryHTML(type_, menu_name_i18n)
                tmp = CategoryMenuBar(self.host) if (count > 1 or not sub_menu) else sub_menu
                current.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=tmp))
            current = tmp
            count -= 1

    def addMenuItem(self, path, path_i18n, type_, menu_cmd):
        """Add a new menu item
        @param path (list[str], str): path to the category, completed by a dummy
        value for the item in last position. Passing a string for the category
        name is also accepted if there's no sub-category.
        @param path_i18n (list[str]): internationalized path to the item
        @param type_ (str): category type in ('games', 'help', 'home', 'photos', 'plugins', 'settings', 'social')
        @param menu_cmd (MenuCmd or PluginMenuCmd): instance to execute as the item callback
        """
        if isinstance(path, str):
            assert(len(path_i18n) == 2)
            path = [path, None]
        assert(len(path) > 1 and len(path) == len(path_i18n))
        log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, type_, menu_cmd))
        sub_menu = self.getCategoryMenu(path[:-1])
        if not sub_menu:
            sub_menu = CategoryMenuBar(self.host)
            self.addCategory(path[:-1], path_i18n[:-1], type_, sub_menu)
        if menu_cmd:
            sub_menu.addItem(path_i18n[-1], menu_cmd)

    def addCachedMenus(self, type_, menu_data=None):
        """Add cached menus to instance
        @param type_: menu type like is sat.core.sat_main.importMenu
        @param menu_data: data to send with these menus
        """
        menus = self.host.menus.get(type_, [])
        for action_id, path, path_i18n in menus:
            if len(path) != len(path_i18n):
                log.error("inconsistency between menu paths")
                continue
            callback = PluginMenuCmd(self.host, action_id, menu_data)
            self.addMenuItem(path, path_i18n, 'plugins', callback)


class CategoryMenuBar(GenericMenuBar):
    """A menu bar for a category (sub-menu)"""
    def __init__(self, host):
        GenericMenuBar.__init__(self, host, vertical=True)