# -*- python -*-
#
# fuss-launcher user interface
#
# Copyright (C) 2010 Enrico Zini <enrico@truelite.it>
# Copyright (C) 2010 Christopher R. Gabriel <cgabriel@truelite.it>
# Copyright (C) 2010 The Fuss Project <info@fuss.bz.it>
#
# Authors: Christopher R. Gabriel <cgabriel@truelite.it>
#          Enrico Zini <enrico@truelite.it>
#
# Sponsored by the Fuss Project: http://www.fuss.bz.it/
#
# 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 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import pygtk
pygtk.require('2.0')
import gtk
import gobject
import gconf
try:
    import dbus
    HAS_DBUS=True
except ImportError:
    HAS_DBUS=False

import fusslauncher
import fusslauncher.appinfo as appinfo

import gettext
_ = gettext.gettext

class AppView(gtk.IconView):
    __gsignals__ = {
        "activated": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, object)),
    }
    def __init__(self):
        super(AppView, self).__init__()
        self.app_cache = appinfo.AppInfoCache()

        self.model = gtk.ListStore(gobject.TYPE_STRING,
                              gtk.gdk.Pixbuf,
                              gobject.TYPE_STRING)
        self.set_model(self.model)

        self.set_text_column(0)
        self.set_pixbuf_column(1)
        self.set_item_width(100)
        self.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, [
            ("text/uri-list", 0, 0),
        ], gtk.gdk.ACTION_DEFAULT|gtk.gdk.ACTION_COPY)
        self.enable_model_drag_dest([
            ("text/uri-list", 0, 0),
        ], gtk.gdk.ACTION_DEFAULT)

        self.views = ["drag", "group", "search"]
        self.desktops = dict([(x, []) for x in self.views])
        self.drag_uris = None
        self.last_drag_item = None
        self.last_drag_uris = None

        self.connect("drag-leave", self.on_drag_leave)
        self.connect("drag-data-get", self.on_drag_data_get)
        self.connect("drag-data-received", self.on_drag_data_received)
        self.connect("drag-motion", self.on_drag_motion)
        self.connect('drag-drop', self.on_drag_drop)
        self.connect('item-activated', self.on_item_activated)

    def on_drag_data_get(self, widget, drag_context, selection_data, info, timestamp):
        print "DDG", widget, drag_context, selection_data, info, timestamp
        cur = widget.get_cursor()
        if not cur: return False
        path, renderer = cur
        i = self.model.get_iter(path)
        d = self.model.get_value(i, 2)
        app = self.app_cache[d]
        uri = "file://" + app.dpath
        print "URI", uri
        selection_data.set_uris((uri,))

    def on_drag_motion(self, widget, drag_context, x, y, timestamp):
        if drag_context.get_source_widget() is not None:
            return False
        #print "MOTION", drag_context, x, y, timestamp
        #print "TARGETS", drag_context.targets
        if self.drag_uris is None:
            widget.drag_get_data(drag_context, "text/uri-list", 0)
        item = self.get_dest_item_at_pos(x, y)
        if item is not None:
            path, pos = item
            #self.set_drag_dest_item(path, gtk.ICON_VIEW_DROP_INTO)
            i = self.model.get_iter(path)
            app = self.model.get_value(i, 2)
            self.last_drag_item = app
        else:
            self.last_drag_item = None
        return False

    def on_drag_leave(self, widget, drag_context, timestamp):
        print "LEAVE", widget, drag_context, timestamp
        if drag_context.get_source_widget() is not None:
            return False
        self.drag_uris = None
        self.set_desktops("drag", [])
        return False

    def on_drag_drop(self, widget, drag_context, x, y, timestamp):
        #print "DROP", widget, drag_context, x, y, timestamp
        if self.last_drag_item is not None:
            app = self.app_cache[self.last_drag_item]
            self.emit('activated', app, self.last_drag_uris)
            drag_context.finish(True, False, timestamp)
        else:
            drag_context.finish(False, False, timestamp)
        return False

    def on_drag_data_received(self, widget, drag_context, x, y, selection_data, info, timestamp):
        #print "DATARECEIVED", widget, drag_context, x, y, selection_data, info, timestamp
        self.set_drag_uris(selection_data.get_uris())
        return False

    def on_item_activated(self, view, path):
        i = self.model.get_iter(path)
        app = self.model.get_value(i, 2)
        self.reset_app_cache()
        app = self.app_cache[app]
        self.emit('activated', app, None)

    def set_drag_uris(self, uris):
        self.drag_uris = uris
        if uris is not None:
            self.last_drag_uris = uris
        desktops = appinfo.mime_db.lookup_uris(uris)
        self.set_desktops("drag", desktops)

    def set_desktops(self, view, desktops):
        self.desktops[view] = desktops
        # Reset 'drag' view when anything else happens
        if view != "drag":
            self.desktops["drag"] = []
        self.model.clear()
        for v in self.views:
            if not self.desktops[v]: continue
            for d in self.desktops[v]:
                app = self.app_cache[d]
                iter = self.model.append(None)
                self.model.set(iter, 0, app.name)
                self.model.set(iter, 1, app.icon)
                self.model.set(iter, 2, d)
            break

    def reset_app_cache(self):
        self.app_cache.reset()

gobject.type_register(AppView)


class Preferred(gtk.IconView):
    __gsignals__ = {
        "activated": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object, object)),
    }
    def __init__(self):
        super(Preferred, self).__init__()
        self.app_cache = appinfo.AppInfoCache()

        self.model = gtk.ListStore(gobject.TYPE_STRING,
                              gtk.gdk.Pixbuf,
                              gobject.TYPE_STRING)
        self.set_model(self.model)

        self.set_columns(1)
        self.set_text_column(0)
        self.set_pixbuf_column(1)
        self.set_item_width(100)

        self.refresh()

        self.drag_pos = None

        self.enable_model_drag_dest([
            ("text/uri-list", 0, 0),
        ], gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_PRIVATE)

        self.add_events(gtk.gdk.KEY_RELEASE_MASK)
        self.connect("key-release-event", self.on_key_released)
        self.connect("drag-motion", self.on_drag_motion)
        self.connect("drag-data-received", self.on_drag_data_received)
        self.connect('drag-drop', self.on_drag_drop)
        self.connect('item-activated', self.on_item_activated)

    def on_key_released(self, widget, event):
        if event.keyval not in [gtk.keysyms.Delete, gtk.keysyms.BackSpace]:
            return False
        cur = self.get_cursor()
        if cur is None: return False
        path, renderer = cur
        i = self.model.get_iter(path)
        app = self.model.get_value(i, 2)
        print "DEL", app
        if appinfo.favourites.remove(app):
            self.refresh()

    def on_drag_motion(self, widget, drag_context, x, y, timestamp):
        #print "MOTION", drag_context, x, y, timestamp
        #print "TARGETS", drag_context.targets
        res = self.get_drag_dest_item()
        if res is None:
            self.drag_pos = None
            return False
        path, pos = res
        if pos != gtk.ICON_VIEW_DROP_INTO:
            self.drag_pos = None
            return False
        i = self.model.get_iter(path)
        self.drag_pos = self.model.get_value(i, 2)
        return False

    def on_drag_drop(self, widget, drag_context, x, y, timestamp):
        print "DROP", widget, drag_context, x, y, timestamp
        widget.drag_get_data(drag_context, "text/uri-list", 0)
        return False

    def on_drag_data_received(self, widget, drag_context, x, y, selection_data, info, timestamp):
        print "DATARECEIVED", widget, drag_context, x, y, selection_data, info, timestamp
        print "URIS", selection_data.get_uris()
        if self.drag_pos:
            # If we are dropping inside an icon, run it
            app = self.app_cache[self.drag_pos]
            self.emit('activated', app, selection_data.get_uris())
        else:
            #print "DROP", widget, drag_context, x, y, timestamp
            def is_desktop(uri):
                return uri.startswith("file://") and uri.endswith(".desktop")
            uris = [x for x in selection_data.get_uris() if is_desktop(x)]
            if not uris:
                drag_context.finish(False, False, timestamp)
            else:
                for u in uris:
                    self.add_desktop(u[7:])
                drag_context.finish(True, False, timestamp)
        return False

    def on_item_activated(self, view, path):
        print "OIA"
        m = self.model
        i = m.get_iter(path)
        app = m.get_value(i, 2)
        self.reset_app_cache()
        app = self.app_cache[app]
        self.emit('activated', app, None)

    def add_desktop(self, desktop):
        print "ADD", desktop
        if appinfo.favourites.add(desktop):
            self.refresh()

    def refresh(self):
        self.model.clear()
        for p in appinfo.favourites.get():
            app = self.app_cache[p]
            iter = self.model.append(None)
            self.model.set(iter, 0, app.name)
            self.model.set(iter, 1, app.icon)
            self.model.set(iter, 2, p)

    def reset_app_cache(self):
        self.app_cache.reset()

gobject.type_register(Preferred)

class Groups(gtk.VButtonBox):
    __gsignals__ = {
        "activated": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (object,)),
    }

    def __init__(self):
        super(Groups, self).__init__()

    def update(self, groups):
        for c in self.get_children():
            self.remove(c)
        for g in sorted(groups, key=lambda x:x.getName()):
            name = g.getName()
            #icon = appinfo.icon_theme.load_icon(g.getIcon(), 24, 0)
            b = gtk.Button(label=name)
            b.set_relief(gtk.RELIEF_NONE)
            b.set_image(gtk.image_new_from_icon_name(g.getIcon(), gtk.ICON_SIZE_MENU))
            b.connect("clicked", self.on_clicked, g)
            b.show()
            self.pack_start(b)

    def on_clicked(self, button, group):
        self.emit('activated', group)

class Links(gtk.VButtonBox):
    def __init__(self):
        super(Links, self).__init__()

    def update(self, links):
        for c in self.get_children():
            self.remove(c)
        for l in sorted(links, key=lambda x:x.getName()):
            name = l.getName()
            url = l.getURL()
            b = gtk.LinkButton(url, label=name)
            #b.set_relief(gtk.RELIEF_NONE)
            b.show()
            self.pack_start(b)

class Launcher(gtk.Window):
    __gsignals__ = {
        "cancelled": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ()),
    }

    def __init__(self):
        super(Launcher, self).__init__(gtk.WINDOW_TOPLEVEL)
        # Do not use WINDOW_POPUP: it makes it show on top of the panel
        #super(Launcher, self).__init__(gtk.WINDOW_POPUP)
        self.set_keep_above(True)
        self.engine = fusslauncher.Engine()
        self.engine.set_install_only(True)
        self.show_preferred = True
        self.show_extras = True
        self.groups = []
        self.links = []
        self.extras = appinfo.Extras()

        #self.set_resizable(False)
        self.set_decorated(False)
        self.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
        self.set_title(_("Fuss application launcher"))
        self.set_position(gtk.WIN_POS_MOUSE);
        self.set_skip_taskbar_hint(True);
        self.set_skip_pager_hint(True);
        self.stick()
        self.set_border_width(5);
#        win.show_all();
#        win.show();

        def hide(*args, **kw):
            self.appView.reset_app_cache()
            self.hide()
            return False
        self.connect("destroy", hide)
        self.connect("delete_event", hide)
        # TODO: Listen to the "window-state-event" (GObject's signal) to detect
        # when minimizing and, instead of doing that, hide the window (ie,
        # "minimize to the tray").

        # Current layout settings
        self.layout_left = None
        self.layout_top = None

        # Build UI components

        #  Search field
        self.entry_frame = self.make_entry_field()

        #  Applications frame
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC,
                      gtk.POLICY_AUTOMATIC)
        self.appView = AppView()
        self.appView.set_size_request(300,200)
        def on_activated(*args, **kw):
            self.hide()
        self.appView.connect("activated", on_activated)
        sw.add(self.appView)
        self.app_frame = gtk.Frame(label=_("Applications"))
        self.app_frame.add(sw)

        #  Preferred frame
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_NEVER,
                      gtk.POLICY_AUTOMATIC)
        self.preferred = Preferred()
        self.preferred.connect("activated", on_activated)
        #self.preferred.set_size_request(400,300)
        #def on_activated(*args, **kw):
        #    self.hide()
        sw.add(self.preferred)
        self.preferred_frame = gtk.Frame(label=_("Favourites"))
        self.preferred_frame.add(sw)

        #  Groups frame
        self.groups = Groups()
        self.groups_frame = gtk.Frame(label=_("Groups"))
        self.groups_frame.add(self.groups)
        def on_activated(groups, group):
            desktops = appinfo.flatten_menu(group)
            self.appView.set_desktops("group", desktops)
        self.groups.connect("activated", on_activated)

        #  Links frame
        self.links = Links()
        self.links_frame = gtk.Frame(label=_("Links"))
        self.links_frame.add(self.links)

        #  Extras frame
        self.extras_frame = gtk.VBox()
        self.extras_frame.pack_start(self.groups_frame, False, False)
        self.extras_frame.pack_start(self.links_frame, False, False)

        #  Button bar
        aboutbutton = gtk.Button(stock="gtk-about")
        aboutbutton.set_relief(gtk.RELIEF_NONE)
        def do_about(*args, **kw):
            run_about()

        aboutbutton.connect("clicked", do_about)
        self.favbutton = gtk.ToggleButton(label=_("Favourites"))
        self.favbutton.set_relief(gtk.RELIEF_NONE)
        self.favbutton.set_active(True)
        self.extrasbutton = gtk.ToggleButton(label=_("Extras"))
        self.extrasbutton.set_relief(gtk.RELIEF_NONE)
        self.extrasbutton.set_active(True)
        self.message_button = gtk.Button(label=_("Ask help"))
        self.message_button.set_relief(gtk.RELIEF_NONE)
        self.message_button.connect("clicked", self.do_admin_message)
        quitbutton = gtk.Button(stock="gtk-close")
        quitbutton.set_relief(gtk.RELIEF_NONE)
        def do_cancel(*args, **kw):
            self.appView.reset_app_cache()
            self.hide()
            self.emit('cancelled')
        quitbutton.connect("clicked", do_cancel)
        self.buttonbar = gtk.HButtonBox()
        self.buttonbar.set_layout(gtk.BUTTONBOX_SPREAD)
        self.buttonbar.pack_start(aboutbutton)
        self.buttonbar.pack_start(self.favbutton)
        self.buttonbar.pack_start(self.extrasbutton)
        self.buttonbar.pack_start(self.message_button)
        self.buttonbar.pack_start(quitbutton)

    def layout(self, left, top):
        if self.layout_left == left and self.layout_top == top:
            return

        # Empty the window
        c = self.get_child();
        if c: self.remove(c)

        # Unparent if needed
        def unparent(x):
            p = x.get_parent()
            if p: p.remove(x)
        unparent(self.app_frame)
        unparent(self.preferred_frame)
        unparent(self.entry_frame)
        unparent(self.extras_frame)
        unparent(self.buttonbar)

        h = gtk.HBox()
        if left:
            h.pack_start(self.extras_frame, False, False)
            h.pack_start(self.app_frame, True, True)
            h.pack_start(self.preferred_frame, False, False)
        else:
            h.pack_start(self.preferred_frame, False, False)
            h.pack_start(self.app_frame, True, True)
            h.pack_start(self.extras_frame, False, False)

        v = gtk.VBox()
        #v.set_border_width(15)
        if top:
            v.pack_start(self.entry_frame, False, False)
            v.pack_start(h, True, True)
            v.pack_start(self.buttonbar, False, False)
        else:
            v.pack_start(self.buttonbar, False, False)
            v.pack_start(h, True, True)
            v.pack_start(self.entry_frame, False, False)

        v.show_all()
        self.preferred_frame.set_visible(self.show_preferred)
        self.extras_frame.set_visible(self.show_extras)
        self.add(v)
        self.set_size_request(600, 400);

        self.layout_left = left
        self.layout_top = top

    def make_entry_field(self):
        h = gtk.HBox()
        l = gtk.Label("<b>%s</b> " % _("Search:"))
        l.set_use_markup(True)
        l.set_justify(gtk.JUSTIFY_RIGHT)
        h.pack_start(l, False, False)
        self.entryFilter = gtk.Entry()
        self.entryCompletion = gtk.EntryCompletion()
        self.entryCompletion.set_inline_completion(True)
        self.entryCompletion.set_text_column(0)
        self.entryFilter.set_completion(self.entryCompletion)
        self.entryFilter.connect("changed", self.update_filter)
        h.pack_start(self.entryFilter, True, True)
        return h

    def set_preferred_visible(self, val):
        self.preferred_frame.set_visible(val)
        self.favbutton.set_active(val)
        self.show_preferred = val

    def set_extras_visible(self, val):
        self.extras_frame.set_visible(val)
        self.extrasbutton.set_active(val)
        self.show_extras = val

    def update_filter(self, entry=None):
        if entry is None: entry = self.entryFilter
        self.appView.set_desktops("group", [])
        query = entry.get_text()
        if not query:
            desktops = appinfo.run_stats.get()[:10]
            completions = []
        else:
            self.engine.set_query(entry.get_text(), [])
            desktops = [x[1] for x in self.engine.documents()]
            completions = self.engine.completions()

        completion_model = gtk.ListStore(gobject.TYPE_STRING)
        for x in completions:
            iter = completion_model.append(None)
            completion_model.set(iter, 0, x)
        self.entryCompletion.set_model(completion_model)

        self.appView.set_desktops("search", desktops)

    def can_do_admin_message(self):
        if not HAS_DBUS: return False
        bus = dbus.SystemBus()
        return bus.name_has_owner("org.octofuss.OctofussClient")

    def do_admin_message(self, b):
        dialog = gtk.Dialog(_("Send a message to administrator"), None,
                            gtk.DIALOG_MODAL,
                            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                             gtk.STOCK_OK, gtk.RESPONSE_OK))
        h = gtk.HBox(False, 8)
        dialog.vbox.pack_start(h)
        stock = gtk.image_new_from_stock(
            gtk.STOCK_DIALOG_QUESTION,
            gtk.ICON_SIZE_DIALOG)
        h.pack_start(stock, False, False, 0)
        v = gtk.VBox()
        h.pack_start(v, False, False)
        v.pack_start(gtk.Label(_("Message")))
        message_entry = gtk.Entry()
        v.pack_start(message_entry, False, False)
        dialog.show_all()

        response = dialog.run()
        if response == gtk.RESPONSE_OK:
            message = message_entry.get_text()
            if message:
                user = os.environ['USERNAME']
                workstation = os.popen("hostname --fqdn").read().strip()
                self.send_admin_message(user, workstation, message)
        dialog.destroy()

    def send_admin_message(self, user, workstation,message):
        try:
            bus = dbus.SystemBus()
            remote_obj = bus.get_object("org.octofuss.OctofussClient","/")
            s = dbus.Interface(remote_obj, 'org.octofuss.Interface')
            result = s.user_question(user, workstation, message)
        except:
            pass

    def toggle(self):
        if self.get_property("visible"):
            self.set_visible(False)
        else:
            # Check where the mouse has been clicked, which is near to where we
            # are, and infer orientation
            disp = self.get_display()
            scr, x, y, mod = disp.get_pointer()
            left = x < scr.get_width() / 2
            top = y < scr.get_height() / 2

            self.layout(left, top)

            # Reread groups and links if needed
            if self.extras.refresh():
                if self.extras.groups:
                    self.groups.update(self.extras.groups)
                    self.groups_frame.set_visible(True)
                else:
                    self.groups_frame.set_visible(False)
                if self.extras.links:
                    self.links.update(self.extras.links)
                    self.links_frame.set_visible(True)
                else:
                    self.links_frame.set_visible(False)

            self.message_button.set_visible(self.can_do_admin_message())

            self.set_visible(True)
            self.entryFilter.set_text("")
            self.entryFilter.grab_focus()
            self.update_filter()


def install_tray_icon(launcher):
    icon = gtk.status_icon_new_from_icon_name("fuss-launcher")
    icon.set_tooltip(_("Fuss launcher"))

    # XPERIM
    #icon.enable_model_drag_dest([
    #    ("text/uri-list", 0, 0),
    #], gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_PRIVATE)
    #def callback2(widget, drag_context, x, y, timestamp):
    #    print "MOTION", drag_context, x, y, timestamp
    #    return False
    #icon.connect("drag-motion", callback2)

    def on_clicked(*args, **kw):
        launcher.toggle()

    icon.connect("activate", on_clicked)
    # icon.connect("popup-menu", ...)
    icon.set_visible(True)

def run_about():
    icon_theme = gtk.icon_theme_get_default()
    about = gtk.AboutDialog()
    about.set_name(_("Fuss Launcher"))
    about.set_copyright(_("Copyright (C) 2010 The Fuss Project"))
    about.set_authors(["Christopher R. Gabriel <cgabriel@truelite.it>",
                       "Enrico Zini <enrico@truelite.it>"])
    about.set_comments(_("A simplified launcher for applications"))
    about.set_logo(icon_theme.load_icon("fuss-launcher", 48, 0))
    about.connect("destroy", about.hide)
    about.run()
    about.hide()

def run_launcher(opts):
    launcher = Launcher()

    gclient = gconf.client_get_default()
    gclient.add_dir('/apps/fuss-launcher', gconf.CLIENT_PRELOAD_NONE)
    def on_fav_changed(client, *args, **kwargs):
        val = client.get_bool("/apps/fuss-launcher/display-favourites")
        launcher.set_preferred_visible(val)
    gclient.notify_add('/apps/fuss-launcher/display-favourites', on_fav_changed)
    def on_extras_changed(client, *args, **kwargs):
        val = client.get_bool("/apps/fuss-launcher/display-extras")
        launcher.set_extras_visible(val)
    gclient.notify_add('/apps/fuss-launcher/display-extras', on_extras_changed)

    on_fav_changed(gclient)
    on_extras_changed(gclient)

    def on_fav_toggled(button):
        val = button.get_active()
        gclient.set_bool("/apps/fuss-launcher/display-favourites", val)
    launcher.favbutton.connect("toggled", on_fav_toggled)

    def on_extras_toggled(button):
        val = button.get_active()
        gclient.set_bool("/apps/fuss-launcher/display-extras", val)
    launcher.extrasbutton.connect("toggled", on_extras_toggled)


    def on_activated(obj, app, args):
        #print "APP", app
        #print "ARG", args
        app.run(args)
    launcher.appView.connect("activated", on_activated)
    launcher.preferred.connect("activated", on_activated)

    install_tray_icon(launcher)
    gtk.main()
