#!/usr/bin/env python
"""
build-toolchest-menus

Utility to create a system wide chestrc file 
for the Maxx desktop toolchest application.

Copyright 2010, Joe Bacom

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
USA.

"""

import sys, os, xml, re, xml.sax.handler

from subprocess import *

menuNames = []
chestNames = []

defaultShell = '/opt/MaXX/bin/winterm'

def get_XML_doc():
    # execute xdg_menu with the following parameters
    # --format xfce4
    # --fullmenu
    # and capture the XML output into string variable

    p = Popen('/usr/bin/xdg_menu --format xfce4 --fullmenu', 
              bufsize=0, stdin=PIPE, stdout=PIPE, close_fds=True, shell=True)

    rawXML = p.stdout.read()
    
    if len(rawXML) < 1:
        print "No XML content from XDG Menu."
        print "Exiting..."
        exit()
    
    xmldoc = XML2obj(rawXML)
    
    build_chest_menu(xmldoc)

def get_menus_list(node):
    
    menus = []
    
    for menu in node.menu:
        menus.append(menu.name)
    
    return menus
    
def get_apps_list(node):
    
    apps = {}
    
    for app in node.app:
        apps[app.name] = {}
        apps[app.name]['name'] = app.name
        apps[app.name]['cmd']  = app.cmd
        apps[app.name]['term'] = app.term  
    
    return apps
    
def process_menu(node):
    
    chest = ""
    
    # This is sub menu
    for menuItem in node.menu:
        hasSubMenu = False
        # Open this chest
        chest += open_chest(menuItem, node.name, False)
        # Check to see if there is another sub menu
        if menuItem.menu:
            hasSubMenu = True
            # There is a sub-menu, get the menu list
            chest += add_menu_list_to_chest(menuItem, chest, False)
        
        # Check to see if there are any apps for this menu
        if menuItem.app:
            # There are apps, get the appl list
            chest += add_app_list_to_chest(menuItem)
            
        if menuItem.name == 'System':
            chest += append_to_system()
            
        chest += close_chest()
        
        if hasSubMenu:
            # Recursively call processMenu until the end
            chest += process_menu(menuItem)
            
    return chest

def open_chest(node, parent, toplevel=False):
    # Open a new chest menu
    
    if toplevel:
        # Open the toplevel chest (ToolChest)
        chest = 'Menu ToolChest\n{\n'
    else:
        menuTag = node.name.replace(' ', '')
        
        # If menuTag is already in chestNames, the menuTag has already been 
        # used
        if menuTag in chestNames:
            menuTag = '%s%s' % (node.name.replace(' ', ''), 
                                parent.replace(' ', ''))
            
        chest = 'Menu %s\n{\n' % menuTag
            
        chestNames.append(menuTag)
        
    return chest

def close_chest():
    # Close the menu list    
    return '}\n\n'

def add_menu_list_to_chest(node, hasApps = False):
    # Adds a set of menu options to the chest
    
    chest = ""
    menus = get_menus_list(node)
    
    x = 1
    if len(menus) > 0:
        menus.sort()
        
    for menu in menus:
        if menu in menuNames:
            menuTag = '%s%s' % (menu, node.name)
        else:
            menuTag = menu
            
        chest += '\t"%s"\t\t\tf.menu %s\n' % (menu, 
                                              menuTag.replace(' ', ''))
        
        # Don't add a separator to the last menu option unless
        # this menu also contains applications
        if x < len(menus) or hasApps:
            chest += '\tseparator\t\t\tf.separator\n'
            
        x += 1
        
        # Add this menu to the menu list for later checking
        menuNames.append(menu)
        
    if node.name == 'System':
        chest += append_to_system()
        
    return chest

def add_app_list_to_chest(node):
    # Adds a set of application(s) options to the chest
    
    chest = ""
    apps = get_apps_list(node)
    
    keys = apps.keys()
    if len(keys) > 0:
        keys.sort()
        
    for k in keys:
        app = apps[k]
        cmd = app['cmd'].replace('"', "'")
        
        if app['term'] == 'yes':
            # The app requires a shell window to launch
            chest += '\t"%s..."\t\t\tf.checkexec.sh.le ' % app['name']
            chest += '"%s %s"\n' % (defaultShell,  cmd)
        else:
            chest += '\t"%s..."\t\t\tf.checkexec.sh.le "%s"\n' % (
                app['name'], cmd)
            
    return chest
    
def append_to_system():
    
    menus = '\tseparator\t\t\tf.separator\n'
    menus += '\tRestart Window Manager\t\t\tf.exec "/opt/MaXX/bin/tellwm restart"\n'
    menus += '\tseparator\t\t\tf.separator\n'
    menus += '\tseparator\t\t\tf.separator\n'
    menus += '\t"Log Out"\t\t\tf.exec "/opt/MaXX/bin/tellwm quit"\n'
    
    return menus
    
def build_chest_menu(xmldoc):
    
    fullchest = '!!\n!! System Menu Description\n!!\n'
    
    # Open the chest for the top level menu
    fullchest += open_chest(xmldoc, None, True)
    
    # Add user level Desktop menu
    fullchest += '\t"Desktop"\t\t\tf.menu Desktop\n'
    fullchest += '\tseparator\t\t\tf.separator\n'
    menuNames.append(u"Desktop")
    chestNames.append(u"Desktop")
    
    # Get the toplevel menus
    fullchest += add_menu_list_to_chest(xmldoc)
    
    # Close the top level menus
    fullchest += close_chest()
    
    # Process each menu in toplevel menus
    for menuItem in xmldoc.menu:
        hasSubMenu = False
        hasApps = False
        # Process menus first so they are at the 
        # top of the drop down / fold out
        
        # Open the chest for this menu
        fullchest += open_chest(menuItem, menuItem.name, False)
        
        if menuItem.app:
            hasApps = True
            
        # Check to see if there is a sub menu
        if menuItem.menu:
            hasSubMenu = True
            # There is a sub-menu, get the menu list
            fullchest += add_menu_list_to_chest(menuItem, hasApps)
            
        # Check to see if there are any apps for this menu
        if menuItem.app:
            # There are apps, get the appl list
            fullchest += add_app_list_to_chest(menuItem)
            
        # Close the chest
        fullchest += close_chest()
        
        if hasSubMenu:
            fullchest += process_menu(menuItem)

    # Write the file out to disk
    fd = open('/tmp/system.chestrc', 'w')
    fd.write(fullchest.encode('UTF-8'))
    fd.close()
    
    return

def XML2obj(src):
    """
       xmlstruct.py
       Module to convert a XML source file to a python data
       structure. 
        
       Adapted from original source by author:  Wai Yip Tung.
       Original source can be found here:
       http://code.activestate.com/recipes/534109/
       
       Example:
       
       >>> SAMPLE_XML = <?xml version="1.0" encoding="UTF-8"?>
       ... <address_book>
       ...   <person gender='m'>
       ...     <name>fred</name>
       ...     <phone type='home'>54321</phone>
       ...     <phone type='cell'>12345</phone>
       ...     <note>&quot;A<!-- comment --><![CDATA[ <note>]]>&quot;</note>
       ...   </person>
       ... </address_book>
       >>> address_book = xml2obj(SAMPLE_XML)
       >>> person = address_book.person
       
       person.gender        -> 'm'     # an attribute
       person['gender']     -> 'm'     # alternative dictionary syntax
       person.name          -> 'fred'  # shortcut to a text node
       person.phone[0].type -> 'home'  # multiple elements becomes an list
       person.phone[0].data -> '54321' # use .data to get the text value
       str(person.phone[0]) -> '54321' # alternative syntax for the text value
       person[0]            -> person  # if there are only one <person>, it can still
                                       # be used as if it is a list of 1 element.
       'address' in person  -> False   # test for existence of an attr or child
       person.address       -> None    # non-exist element returns None
       bool(person.address) -> False   # has any 'address' data (attr, child or text)
       person.note          -> '"A <note>"'
    """
    
    non_id_char = re.compile('[^_0-9a-zA-Z]')
    def _name_mangle(name):
        return non_id_char.sub('_', name)

    class DataNode(object):
        def __init__(self):
            # XML attributes and child elements
            self._attrs = {}
             # child text data
            self.data = None
        def __len__(self):
            # treat single element as a list of 1
            return 1
        def __getitem__(self, key):
            if isinstance(key, basestring):
                return self._attrs.get(key,None)
            else:
                return [self][key]
        def __contains__(self, name):
            return self._attrs.has_key(name)
        def __nonzero__(self):
            return bool(self._attrs or self.data)
        def __getattr__(self, name):
            if name.startswith('__'):
                # need to do this for Python special methods???
                raise AttributeError(name)
            return self._attrs.get(name,None)
        def _add_xml_attr(self, name, value):
            if name in self._attrs:
                # multiple attribute of the same name are represented by a list
                children = self._attrs[name]
                if not isinstance(children, list):
                    children = [children]
                    self._attrs[name] = children
                children.append(value)
            else:
                self._attrs[name] = value
        def __str__(self):
            return self.data or ''
        def __repr__(self):
            items = sorted(self._attrs.items())
            if self.data:
                items.append(('data', self.data))
            return '{%s}' % ', '.join(['%s:%s' % (k,repr(v)) for k,v in items])

    class TreeBuilder(xml.sax.handler.ContentHandler):
        def __init__(self):
            self.stack = []
            self.root = DataNode()
            self.current = self.root
            self.text_parts = []
        def startElement(self, name, attrs):
            self.stack.append((self.current, self.text_parts))
            self.current = DataNode()
            self.text_parts = []
            # xml attributes --> python attributes
            for k, v in attrs.items():
                self.current._add_xml_attr(_name_mangle(k), v)
        def endElement(self, name):
            text = ''.join(self.text_parts).strip()
            if text:
                self.current.data = text
            if self.current._attrs:
                obj = self.current
            else:
                # a text only node is simply represented by the string
                obj = text or ''
            self.current, self.text_parts = self.stack.pop()
            self.current._add_xml_attr(_name_mangle(name), obj)
        def characters(self, content):
            self.text_parts.append(content)

    builder = TreeBuilder()
    if isinstance(src,basestring):
        xml.sax.parseString(src, builder)
    else:
        xml.sax.parse(src, builder)
    return builder.root._attrs.values()[0]

if __name__ == '__main__':
    get_XML_doc()
        
