# This file is part of MyPaint.
# Copyright (C) 2009 by Martin Renold <martinxyz@gmx.ch>
#
# 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.

import gtk
gdk = gtk.gdk
import pygtkcompat

class KeyboardManager:
    """
    This class represents all keyboard shortcuts (similar to
    gtk.AccelGroup). It connects to keyboard events of various
    gtk.Window instances to handle hotkeys. It synchronizes with the
    global gtk accelmap to figure out what keyboard shortcuts the user
    has assigned through the menu.

    The point of the whole exercise (instead of just using gtk
    standard tools) is to allow the action handlers to wait for the
    corresponding key release event.

    This class adds extra state attributes to every gtk.Action.
    """
    def __init__(self, app):
        self.enabled = True
        self.actions = []
        self.keymap  = {} # (keyval, modifiers) --> gtk.Action
        self.keymap2 = {} # (keyval, modifiers) --> gtk.Action (2nd priority; for hardcoded keys)
        self.pressed = {} # hardware_keycode --> gtk.Action (while holding it down)
        self.app = app

    def start_listening(self):
        pygtkcompat.gtk.accel_map_get().connect('changed', self.accel_map_changed_cb)

    def accel_map_changed_cb(self, object, accel_path, accel_key, accel_mods):
        self.update_keymap(accel_path)

    def update_keymap(self, accel_path):
        if not accel_path:
            return
        for k, v in self.keymap.items():
            if v.get_accel_path() == accel_path:
                del self.keymap[k]

        shortcut = pygtkcompat.gtk.accel_map_lookup_entry(accel_path)
        if shortcut:
            for action in self.actions:
                if action.get_accel_path() == accel_path:
                    self.keymap[shortcut] = action
                    return
            print 'Ignoring keybinding for', accel_path

    def key_press_cb(self, widget, event):
        if not self.enabled:
            return
        # See gtk sourcecode in gtkmenu.c function gtk_menu_key_press,
        # which uses the same code as below when changing an accelerator.
        keymap = pygtkcompat.gdk.keymap_get_default()
        # Instead of using event.keyval, we do it the lowlevel way.
        # Reason: ignoring CAPSLOCK and checking if SHIFT was pressed
        state = event.state & ~gdk.LOCK_MASK
        if pygtkcompat.USE_GTK3:
            state = gdk.ModifierType(state)
        res = keymap.translate_keyboard_state(event.hardware_keycode, state,
                                              event.group)
        if not res:
            # PyGTK returns None when gdk_keymap_translate_keyboard_state() returns false.
            # Not sure if this is a bug or a feature - the only time I have seen this
            # happen is when I put my laptop into sleep mode.
            print 'Warning: translate_keyboard_state() returned None. Strange key pressed?'
            return

        keyval_offset = 1 if pygtkcompat.USE_GTK3 else 0
        keyval = res[keyval_offset]
        consumed_modifiers = res[keyval_offset+3]
        # We want to ignore irrelevant modifiers like ScrollLock.
        # The stored key binding does not include modifiers that affected its keyval.
        modifiers = event.state & gtk.accelerator_get_default_mod_mask() & ~consumed_modifiers
        # Except that key bindings are always stored in lowercase.
        keyval_lower = gdk.keyval_to_lower(keyval)
        if keyval_lower != keyval:
            modifiers |= gdk.SHIFT_MASK
        action = self.keymap.get((keyval_lower, modifiers))
        if not action:
            # try hardcoded keys
            action = self.keymap2.get((keyval_lower, modifiers))

        return self.activate_keydown_event(action, event)

    def activate_keydown_event(self, action, event):
        # The kbm is responsible for activating events which correspond to
        # keypresses so that it can keep track of which keys are pressed.
        # Expose this part on a separate method so that canvas "pointer" events
        # using the Space=Button2 equivalence can invoke popup states via their
        # action as proper keypresses.
        if action:
            def activate():
                action.keydown = True
                action.keyup_callback = None
                action.activate()
                action.keydown = False

            if event.hardware_keycode in self.pressed:
                # allow keyboard autorepeating only if the action
                # handler is not waiting for the key release event
                if not action.keyup_callback:
                    activate()
            else:
                #print 'PRESS', action.get_name()
                self.pressed[event.hardware_keycode] = action
                ## make sure we also get the corresponding key release event
                #gdk.keyboard_grab(widget.window, False, event.time)
                #widget.grab_add() hm? what would this do?
                activate()
            return True

    def key_release_cb(self, widget, event):
        def released(hardware_keycode):
            #gdk.keyboard_ungrab(event.time)
            action = self.pressed[hardware_keycode]
            del self.pressed[hardware_keycode]
            #print 'RELEASE', action.get_name()
            if action.keyup_callback:
                action.keyup_callback(widget, event)
                action.keyup_callback = None

        if event.keyval == gtk.keysyms.Escape:
            # emergency exit in case of bugs
            for hardware_keycode in self.pressed.keys():
                released(hardware_keycode)
            # Pop all stacked modes; they should release grabs
            self.app.doc.modes.reset()
            # Just in case...
            gdk.pointer_ungrab(event.time)
        else:
            # note: event.keyval would not be suited for this because
            # it can be different from the one we have seen in
            # key_press_cb if the user has released a modifier first
            if event.hardware_keycode in self.pressed:
                released(event.hardware_keycode)
                return True

    def add_window(self, window):
        window.connect("key-press-event", self.key_press_cb)
        window.connect("key-release-event", self.key_release_cb)

    def add_extra_key(self, keystring, action):
        keyval, modifiers = gtk.accelerator_parse(keystring)
        if callable(action):
            # construct an action-like object from a function
            self.add_custom_attributes(action)
            action.activate = lambda: action(action)
            #action.get_name = lambda: action.__name__
        else:
            # find an existing gtk.Action by name
            res = [a for a in self.actions if a.get_name() == action]
            assert len(res) == 1, 'action %s not found, or found more than once' % action
            action = res[0]
        self.keymap2[(keyval, modifiers)] = action

    def takeover_action(self, action):
        assert action not in self.actions
        self.add_custom_attributes(action)
        self.actions.append(action)
        self.update_keymap(action.get_accel_path())

    def add_custom_attributes(self, action):
        assert not hasattr(action, 'keydown')
        assert not hasattr(action, 'keyup_callback')
        action.keydown = False
        action.keyup_callback = None

