#!/usr/bin/env python

#  Smeg - Simple Menu Editor for GNOME
#
#  Travis Watkins <alleykat@gmail.com>
#
#  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.
#
#  (C) Copyright 2005 Travis Watkins

import os, sys, pickle, cgi
cmddir = os.path.split(sys.argv[0])[0]
prefix = os.path.split(cmddir)[0]
if prefix == '': prefix = '.'
libdir = os.path.join(prefix, 'lib/smeg')
sys.path = [libdir] + sys.path

from MenuHandler import MenuHandler
from DialogHandler import DialogHandler
import config
import optparse

import pygtk
pygtk.require('2.0')
import gtk, gtk.glade, gobject
import xdg.Menu

class Smeg(object):

    dnd_targets = [('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_APP, 0)]
    menu_path = None
    entry_path = None
    allow_move = True
    advanced_features = True

    def __init__(self):
        self.tree = gtk.glade.XML(os.path.join(libdir, 'smeg.glade'))
        self.w = self.tree.get_widget
        self.w('prefcustomtheme').set_group(self.w('prefdefaulttheme'))
        self.config = config.Configuration()
        parser = optparse.OptionParser()
        parser.add_option('--gnome', help='Use GNOME settings',
            action='store_true', dest='gnome', default=True)
        parser.add_option('--kde', help='Use KDE settings',
            action='store_true', dest='kde', default=False)
        parser.add_option('--root', help='Edit root file if current user is root',
            action='store_true', dest='root_mode', default=False)
        self.options, args = parser.parse_args()
        self.handler = MenuHandler(self, self.options)

    def setIcon(self):
        if self.handler.desktop_environment == 'GNOME':
            icon = self.getIcon('gnome-main-menu')
        else:
            icon = self.getIcon('kmenu')
        self.w('mainwindow').set_icon(icon)
        self.w('menudialog').set_icon(icon)
        self.w('entrydialog').set_icon(icon)
        self.icon = icon

    def getIcon(self, entry, size=24):
        path = self.handler.getIconPath(entry, size)
        if path == None:
            pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, size, size)
            pb.fill(0x00000000)
        else:
            try:
                pb = gtk.gdk.pixbuf_new_from_file_at_size(path, size, size)
            except:
                pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, size, size)
                pb.fill(0x00000000)
        return pb

    def setupMenusTree(self):
        menus = self.tree.get_widget('menus')
        column = gtk.TreeViewColumn('Name')
        column.set_spacing(6)

        cell = gtk.CellRendererPixbuf()
        column.pack_start(cell, False)
        column.set_attributes(cell, pixbuf=0)

        cell = gtk.CellRendererText()
        column.pack_start(cell, True)
        column.set_attributes(cell, markup=1)
        menus.append_column(column)
        menus.set_expander_column(column)

        self.menu_store = gtk.TreeStore(gtk.gdk.Pixbuf, str, object)
        menus.set_model(self.menu_store)
        menus.enable_model_drag_dest(self.dnd_targets, gtk.gdk.ACTION_PRIVATE)
        menus.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, self.dnd_targets,
            gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_DEFAULT)
        menus.connect('drag_data_received', self.on_menus_drag_data_received_data)
        menus.connect('drag_data_get', self.on_menus_drag_data_get_data)

    def setupEntriesTree(self):
        entries = self.tree.get_widget('entries')
        column = gtk.TreeViewColumn('Visible')

        cell = gtk.CellRendererToggle()
        cell.connect('toggled', self.on_entry_visible_toggled)
        column.pack_start(cell, True)
        column.set_attributes(cell, active=0)
        entries.append_column(column)

        column = gtk.TreeViewColumn('Name')
        column.set_spacing(6)

        cell = gtk.CellRendererPixbuf()
        column.pack_start(cell, False)
        column.set_attributes(cell, pixbuf=1)

        cell = gtk.CellRendererText()
        column.pack_start(cell, True)
        column.set_attributes(cell, markup=2)
        entries.append_column(column)

        self.entry_store = gtk.ListStore(bool, gtk.gdk.Pixbuf, str, object)
        entries.set_model(self.entry_store)
        if self.advanced_features:
            entries.enable_model_drag_dest(self.dnd_targets, gtk.gdk.ACTION_PRIVATE)
        entries.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, self.dnd_targets,
            gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_DEFAULT)
        entries.connect('drag_data_received', self.on_entries_drag_data_received_data)
        entries.connect('drag_data_get', self.on_entries_drag_data_get_data)

    def getMenus(self):
        self.menu_store.clear()
        self.handler.loadMenus()
        if self.menu_path != None:
            self.w('menus').expand_to_path(self.menu_path)
            self.w('menus').get_selection().select_path(self.menu_path)
            self.w('menus').collapse_row(self.menu_path)
        for menu in self.menu_store:
            self.w('menus').expand_to_path(menu.path)

    def addMenu(self, menu, depths, depth):
        pixbuf = self.getIcon(menu)
        show = True
        if menu.Show != True:
            show = False
        name = cgi.escape(menu.getName())
        if show == False:
            name = '<span foreground="#888888">' + name + '</span>'
        return self.menu_store.append(depths[depth-1], [pixbuf, name, menu])

    def getEntries(self, menu):
        self.entry_store.clear()
        for entry in self.handler.loadEntries(menu):
            pixbuf = self.getIcon(entry)
            show = True
            if entry.Show != True:
                show = False
            if isinstance(entry, xdg.Menu.Separator):
                show = True
                name = '-----'
            elif isinstance(entry, xdg.Menu.MenuEntry):
                name = cgi.escape(entry.DesktopEntry.getName())
            else:
                name = cgi.escape(entry.getName())
            if show == False:
                name = '<span foreground="#888888">' + name + '</span>'
            self.entry_store.append([show, pixbuf, name, entry])

    def reloadSmeg(self):
        self.getMenus()
        if self.menu_path:
            self.getEntries(self.menu_store[self.menu_path][2])
        if self.entry_path:
            self.w('entries').get_selection().select_path(self.entry_path)
        self.allow_move = True

    def getLocation(self):
        if self.entry_path:
            item = self.entry_store[self.entry_path][3]
            parent = item.Parent
            index = parent.Entries.index(item)
        elif self.menu_path:
            item = self.menu_store[self.menu_path][2]
            parent = item
            index = len(parent.Entries) - 1
        else:
            item = self.handler.editor.menu
            return item, item, None, None
        if item == self.handler.editor.menu:
            return item, item, None, None
        if index == -1:
            return item, parent, None, None
        if index != 0:
            before = parent.Entries[index - 1]
        else:
            before = parent.Entries[index]
        if index < len(parent.Entries) - 1:
            after = parent.Entries[index + 1]
        else:
            after = parent.Entries[index]
        if not self.advanced_features:
            before, after = None, None
        return item, parent, before, after

    def setAllowed(self, entry):
        self.w('editdelete').set_sensitive(False)
        self.w('editrevert').set_sensitive(False)
        self.w('deletepopup').set_sensitive(False)
        self.w('revertpopup').set_sensitive(False)
        self.w('editproperties').set_sensitive(True)
        self.w('propertiespopup').set_sensitive(True)
        if isinstance(entry, xdg.Menu.Separator):
            self.w('editproperties').set_sensitive(False)
            self.w('propertiespopup').set_sensitive(False)
            self.w('editdelete').set_sensitive(True)
            self.w('deletepopup').set_sensitive(True)
            return
        value = self.handler.getAccess(entry)
        if value == 'none':
            return
        elif value == 'revert':
            self.w('editrevert').set_sensitive(True)
            self.w('revertpopup').set_sensitive(True)
        elif value == 'delete':
            self.w('editdelete').set_sensitive(True)
            self.w('deletepopup').set_sensitive(True)

    #menu callbacks
    def on_filenewmenu_activate(self, menu):
        dialogs = DialogHandler(self)
        item, parent, before, after = self.getLocation()
        name, comment, icon = dialogs.newMenuDialog()
        if not self.entry_path:
            parent = item
            after = None
        else:
            after = item
        if self.entry_path:
            self.entry_path = (self.entry_path[0] + 1,)
        self.handler.newMenu(parent, name, comment, icon, after)
        gobject.timeout_add(120, self.reloadSmeg)

    def on_filenewentry_activate(self, menu):
        dialogs = DialogHandler(self)
        item, parent, before, after = self.getLocation()
        if not self.entry_path:
            parent = item
            after = None
        else:
            after = item
        if self.entry_path:
            self.entry_path = (self.entry_path[0] + 1,)
        name, comment, command, icon, term = dialogs.newEntryDialog()
        self.handler.newEntry(parent, name, comment, command, icon, term, after)
        gobject.timeout_add(120, self.reloadSmeg)

    def on_filenewseparator_activate(self, menu):
        item, parent, before, after = self.getLocation()
        if self.entry_path:
            after = item
        if self.entry_path:
            self.entry_path = (self.entry_path[0] + 1,)
        self.handler.newSeparator(parent, after)
        gobject.timeout_add(120, self.reloadSmeg)

    def on_filequit_activate(self, menu):
        if self.options.root_mode:
            dialog = gtk.MessageDialog(self.w('mainwindow'), gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK_CANCEL, 'Do you want to save root changes? These cannot be undone.')
            response = dialog.run()
            if response == gtk.RESPONSE_CANCEL:
                gtk.main_quit()
                return
        self.handler.quit()
        gtk.main_quit()

    def on_editdelete_activate(self, menu):
        item, parent, before, after = self.getLocation()
        if isinstance(item, xdg.Menu.Menu):
            self.handler.deleteMenu(item)
        elif isinstance(item, xdg.Menu.Separator):
            self.handler.deleteSeparator(item)
        elif isinstance(item, xdg.Menu.MenuEntry):
            self.handler.deleteEntry(item)
        if self.entry_path and len(parent.Entries) == 0:
            self.entry_path = None
        gobject.timeout_add(120, self.reloadSmeg)

    def on_editproperties_activate(self, menu):
        dialogs = DialogHandler(self)
        item, parent, before, after = self.getLocation()
        if isinstance(item, xdg.Menu.Menu):
            name, comment, icon = dialogs.setupMenuDialog(item.getName(),
                item.getComment(), item.getIcon(), self.getIcon(item, 48))
            if name != None:
                self.handler.saveMenu(item, name, comment, icon)
        elif isinstance(item, xdg.Menu.MenuEntry):
            deskentry = item.DesktopEntry
            name, comment, command, icon, term = dialogs.setupEntryDialog(
                deskentry.getName(), deskentry.getComment(),
                deskentry.getExec(), deskentry.getIcon(),
                self.getIcon(deskentry, 48), deskentry.getTerminal())
            if name != None:
                self.handler.saveEntry(item, name, comment, command, icon, term)
        gobject.timeout_add(120, self.reloadSmeg)

    def on_editrevert_activate(self, menu):
        item, parent, before, after = self.getLocation()
        if isinstance(item, xdg.Menu.MenuEntry):
            self.handler.revertEntry(item)
        elif isinstance(item, xdg.Menu.Menu):
            self.handler.revertMenu(item)
        gobject.timeout_add(120, self.reloadSmeg)

    def on_helpabout_activate(self, menu):
        about = gtk.AboutDialog()
        if self.handler.desktop_environment == 'GNOME':
            about.set_icon(self.getIcon('gnome-main-menu'))
            about.set_logo(self.getIcon('gnome-main-menu', 64))
        else:
            about.set_icon(self.getIcon('kmenu'))
            about.set_logo(self.getIcon('kmenu', 64))
        about.set_authors(['Travis Watkins <alleykat@gmail.com>'])
        about.set_name('Smeg')
        about.set_version(config.version)
        about.set_comments('Simple fd.o Complient Menu Editor')
        about.connect('response', lambda *a: about.hide())
        about.show()

    #drag and drop stuff
    def on_menus_drag_data_get_data(self, treeview, context, selection, target_id, etime):
        model, iter = treeview.get_selection().get_selected()
        self.drag_path = self.menu_store.get_path(self.w('menus').get_selection().get_selected()[1])
        path = pickle.dumps(model.get_path(iter))
        selection.set(selection.target, 8, 'menus:::' + path)

    def on_entries_drag_data_get_data(self, treeview, context, selection, target_id, etime):
        model, iter = treeview.get_selection().get_selected()
        self.drag_path = self.menu_store.get_path(self.w('menus').get_selection().get_selected()[1])
        path = pickle.dumps(model.get_path(iter))
        selection.set(selection.target, 8, 'entries:::' + path)

    def on_entries_drag_data_received_data(self, treeview, context, x, y, selection, info, etime):
        model = treeview.get_model()
        source, source_path = selection.data.split(':::', 1)
        source_path = pickle.loads(source_path)
        drop_info = treeview.get_dest_row_at_pos(x, y)
        if drop_info:
            path, position = drop_info
            iter = model.get_iter(path)
            if path == source_path:
                return False
            if source == 'entries':
                before = None
                after = None
                entry = self.entry_store[source_path][3]
                if position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE:
                    before = model[path][3]
                    self.entry_path = path
                    model.insert_before(iter, self.entry_store[source_path])
                else:
                    after = model[path][3]
                    self.entry_path = (path[0] + 1,)
                    model.insert_after(iter, self.entry_store[source_path])
                self.handler.moveEntry(entry, entry.Parent, entry.Parent,
                    before, after, drag=True)
            else:
                return False
        else:
            if source == 'entries':
                path = (len(model) - 1,)
                iter = model.get_iter(path)
                entry = self.entry_store[source_path][3]
                model.insert_after(iter, self.entry_store[source_path])
                after = model[path][3]
                self.entry_path = path
                self.handler.moveEntry(entry, entry.Parent, entry.Parent,
                    after=after, drag=True)
            else:
                return False
        gobject.timeout_add(120, self.reloadSmeg)
        context.finish(True, True, etime)

    def on_menus_drag_data_received_data(self, treeview, context, x, y, selection, info, etime):
        model = treeview.get_model()
        source, source_path = selection.data.split(':::', 1)
        source_path = pickle.loads(source_path)
        drop_info = treeview.get_dest_row_at_pos(x, y)
        if drop_info:
            path, position = drop_info
            if position not in (gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
                return False
            if source == 'entries':
                entry = self.entry_store[source_path][3]
                if isinstance(entry, xdg.Menu.Separator):
                    return False
                if isinstance(entry, xdg.Menu.MenuEntry):
                    self.handler.moveEntry(entry, entry.Parent, model[path][2])
                elif isinstance(entry, xdg.Menu.Menu):
                    if model[path][2] == entry.Parent:
                        return False
                    self.handler.moveMenu(entry, entry.Parent, model[path][2])
                self.menu_path = path
                self.entry_path = None
            elif source == 'menus':
                if path == source_path:
                    return False
                menu = self.menu_store[source_path][2]
                if model[path][2].getName() == 'Other':
                    return False
                self.entry_path = None
                self.menu_path = path
                if model[path][2] != self.handler.editor.menu:
                    if model[path][2].Parent == menu.Parent:
                        source_index = menu.Parent.Entries.index(menu)
                        dest_index = menu.Parent.Entries.index(model[path][2])
                        if source_index < dest_index:
                            self.menu_path = path[0:-1] + (path[-1] - 1,)
                parent = model[path][2]
                while parent != self.handler.editor.menu:
                    if parent == menu:
                        return False
                    parent = parent.Parent
                self.handler.moveMenu(menu, menu.Parent, model[path][2])
            gobject.timeout_add(120, self.reloadSmeg)
        context.finish(True, True, etime)
        return

    #menu treeview callbacks
    def on_menus_cursor_changed(self, treeview):
        model, iter = treeview.get_selection().get_selected()
        path = model.get_path(iter)
        menu = model[path][2]
        self.setAllowed(menu)
        self.menu_path = path
        self.entry_path = None
        self.w('entries').get_selection().unselect_all()
        self.getEntries(menu)

    #entry treeview callbacks
    def on_entry_visible_toggled(self, cell, path):
        entry = self.entry_store[path][3]
        if isinstance(entry, xdg.Menu.Menu) and entry.Show == 'Empty':
            return
        if not isinstance(entry, xdg.Menu.Separator):
            self.entry_store[path][0] = not self.entry_store[path][0]
            self.handler.toggleVisible(entry, not self.entry_store[path][0])
            gobject.timeout_add(120, self.reloadSmeg)

    def on_entries_cursor_changed(self, treeview):
        model, iter = treeview.get_selection().get_selected()
        path = model.get_path(iter)
        self.setAllowed(model[path][3])
        self.entry_path = path

    def on_entries_row_activated(self, *args):
        self.on_editproperties_activate(None)

    #creation buttons callbacks
    def on_newmenu_clicked(self, button):
        self.on_filenewmenu_activate(None)

    def on_newentry_clicked(self, button):
        self.on_filenewentry_activate(None)

    def on_newseparator_clicked(self, button):
        self.on_filenewseparator_activate(None)

    #move buttons callbacks
    def on_moveup_clicked(self, button):
        if not self.allow_move:
            return
        item, parent, before, after = self.getLocation()
        if self.handler.moveEntry(item, parent, parent, before=before):
            self.entry_path = (self.entry_path[0] - 1,)
        self.allow_move = False
        gobject.timeout_add(120, self.reloadSmeg)

    def on_movedown_clicked(self, button):
        if not self.allow_move:
            return
        item, parent, before, after = self.getLocation()
        if self.handler.moveEntry(item, parent, parent, after=after):
            self.entry_path = (self.entry_path[0] + 1,)
        self.allow_move = False
        gobject.timeout_add(120, self.reloadSmeg)

    #right click menu callbacks
    def on_tree_button_press_event(self, treeview, event):
        if event.button == 3:
            x = int(event.x)
            y = int(event.y)
            time = event.time
            pthinfo = treeview.get_path_at_pos(x, y)
            if pthinfo != None:
                path, col, cellx, celly = pthinfo
                model = treeview.get_model()
                iter = model.get_iter(path)
                if model.iter_has_child(iter):
                    if model.iter_parent(iter) == None:
                        return 1
                treeview.grab_focus()
                treeview.set_cursor(path, col, 0)
                self.w('popup').popup( None, None, None, event.button, time)
            return 1

    def on_deletepopup_activate(self, menu):
        self.on_editdelete_activate(None)

    def on_revertpopup_activate(self, menu):
        self.on_editrevert_activate(None)

    def on_propertiespopup_activate(self, menu):
        self.on_editproperties_activate(None)

    #and we're done
    def on_mainwindow_destroy(self, window):
        self.on_filequit_activate(None)

    def run(self):
        signals = {}
        for attr in dir(self):
            signals[attr] = getattr(self, attr)
        self.tree.signal_autoconnect(signals)

        if self.handler.desktop_environment == 'GNOME':
            try:
                import commands
                version = commands.getoutput('pkg-config --modversion libgnome-menu')[2:4]
                if version < '11':
                    if not self.config['warned_about_gnome_menus']:
                        dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, 'GNOME 2.10 does not support menu reordering or separators. These features will be disabled. Please upgrade to a newer version of GNOME.')
                        dialog.run()
                        dialog.destroy()
                        self.config['warned_about_gnome_menus'] = True
                    self.advanced_features = False
                    self.w('newseparator').hide()
                    self.w('filenewseparator').hide()
                    self.w('movebox').hide()
            except:
                pass
        self.setIcon()
        self.setupMenusTree()
        self.setupEntriesTree()
        self.getMenus()
        self.w('mainwindow').show()
        gtk.main()

def main():
    smeg = Smeg()
    smeg.run()

if __name__ == '__main__':
    main()
