#!/usr/bin/python
#pyhk.py
#
# Copyright (C) 2011 Michael Schurpf
# E-mail: michaelschuerpf AT gmail DOT com
#
# Extends pyhook to have normal hotkey functionality like autohotkey (AHK) scripts.
# Hotkeys have to be entered in lists! Even if single items.
# Hotkeys can be entered in ID list or human readble.
# Example: human readble: ['Lcontrol','7'] for left CTRL 7.
# or ['Ctrl','7'] for CTRL 7
# mouse example: ['mouse left','A'] for mouse left and A together
# See createKeyLookup function for all labels,
#
# Remark:
# mouse move, mouse wheel up, mouse wheel down can only be used on its own
# Example: ['mouse wheel down']
# Not working ['mouse wheel down','4']
#
# Scripts maintained at http://www.swisstard.com
# Comments, suggestions and bug reports welcome.
#
# Released subject to the GNU Public License
#
# 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.
#
# In addition to the permissions in the GNU General Public License, the
# authors give you unlimited permission to link or embed the compiled
# version of this file into combinations with other programs, and to
# distribute those combinations without any restriction coming from the
# use of this file. (The General Public License restrictions do apply in
# other respects; for example, they cover modification of the file, and
# distribution when not linked into a combine executable.)
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

import pythoncom, pyHook, ctypes, thread

class pyhk:
    """Hotkey class extending pyhook"""

    def __init__(self ):
        #initiate internal hotkey list
        self.KeyDownID = []
        self.KeyDown = []

        #initiate user hotkey list
        self.UserHKF = []
        self.UserHKFUp = []
        self.HKFIDDict = {}

        #create Lookup for event keys and ids
        #for keyboards
        self.ID2Key, self.Key2ID = self.createKeyLookup()
        #for mouse, artifical lookup first
        self.mouseDown_MID2eventMessage, self.mouseDown_eventMessage2MID, self.mouseUp_MID2eventMessage, self.mouseUp_eventMessage2MID   = self.createMouseLookup()

        #create list for singleEvent, ie there is only a key down, no key up
        self.singleEventMouseMessage, self.singleEventMID = self.createSingleEventMouse()

        #creat list for merged keys like Ctrl <= Lcontrol, Rcontrol
        self.KeyID2MEID = self.createMergeKeys()

        # create a hook manager
        self.hm = pyHook.HookManager()

        # watch for all keyboard events
        self.hm.KeyDown = self.OnKeyDown
        self.hm.KeyUp = self.OnKeyUp

        # watch for all mouse events
        self.hm.MouseAllButtonsDown = self.OnKeyDown
        self.hm.MouseAllButtonsUp = self.OnKeyUp

        self.hm.MouseMove = self.OnSingleEventMouse
        self.hm.MouseWheel = self.OnSingleEventMouse

        # set the hook
        self.hm.HookKeyboard()
        self.hm.HookMouse()

        #set Ending hotkey
        self.EndHotkey = ['Ctrl','Shift','Q']
        self.setEndHotkey(self.EndHotkey)

    def start(self):
        """Start pyhk to check for hotkeys"""
        pythoncom.PumpMessages()

    def end(self):
        """End pyhk to check for hotkeys"""
        ctypes.windll.user32.PostQuitMessage(0)

#--------------------------------------------------------

    def isIDHotkey(self,hotkey):
        """Test if hotkey is coded in IDs"""
        for key in hotkey:
            if type(key) == str:
                return False
        return True

    def isHumanHotkey(self,hotkey):
        """Test if hotkey is coded human readable. Ex ALT F2"""
        try:
            [self.Key2ID[key] for key in hotkey]
        except:
            return False
        return True

    def hotkey2ID(self,hotkey):
        """Converts human readable hotkeys to IDs"""
        if self.isHumanHotkey(hotkey):
            return [self.Key2ID[key] for key in hotkey]
        else:
            raise Exception("Invalid Hotkey")

    def getHotkeyList(self,hotkey):
        """Create a IDlist of hotkeys if necessary to ensure functionality of merged hotkeys"""
        hotkeyVariationList = []
        hotkeyList = []

        #convert everyting into ID,MID,MEID
        if self.isIDHotkey(hotkey):
            IDHotkey = hotkey
        else:
            IDHotkey = self.hotkey2ID(hotkey)

        IDHotkeyTemp = IDHotkey[:]

        #check if there is a MEID and create accorind hotkeyVariationList
        for Key in self.KeyID2MEID:

            if self.KeyID2MEID[Key] in IDHotkeyTemp:
                #merged hotkey in hotkey
                #get MEID
                MEIDTemp = self.KeyID2MEID[Key]
                #get all KeyIDs
                KeyIDVariationTemp = [k for k in self.KeyID2MEID if self.KeyID2MEID[k] == MEIDTemp]

                #remove MEID from IDHotekey
                IDHotkeyTemp.remove(MEIDTemp)

                #store according MEID and KeyIDList
                hotkeyVariationList.append(KeyIDVariationTemp)

        if len(hotkeyVariationList) > 0:
            hotkeyVariationList.append(IDHotkeyTemp)
            #get all possible permutations
            hotkeyList = UniquePermutation(hotkeyVariationList)
        else:
            hotkeyList = [IDHotkey]

        return hotkeyList

    def addHotkey(self,hotkey,fhot, isThread=False, up = False):
        """Add hotkeys with according function"""
        hotkeyList = self.getHotkeyList(hotkey)

        newHKFID = self.getNewHKFID()
        self.HKFIDDict[newHKFID] = []

        if up:

            if len(hotkey)<2:
                if isThread:
                    t = ExecFunThread(fhot)
                    for IDHotKeyItem in hotkeyList:
                        self.UserHKFUp.append([IDHotKeyItem,t.Start])
                        self.HKFIDDict[newHKFID].append([IDHotKeyItem,t.Start])

                else:
                    for IDHotKeyItem in hotkeyList:
                        self.UserHKFUp.append([IDHotKeyItem,fhot])
                        self.HKFIDDict[newHKFID].append([IDHotKeyItem,fhot])

        else:

            if isThread:
                t = ExecFunThread(fhot)
                for IDHotKeyItem in hotkeyList:
                    self.UserHKF.append([IDHotKeyItem,t.Start])
                    self.HKFIDDict[newHKFID].append([IDHotKeyItem,t.Start])

            else:
                for IDHotKeyItem in hotkeyList:
                    self.UserHKF.append([IDHotKeyItem,fhot])
                    self.HKFIDDict[newHKFID].append([IDHotKeyItem,fhot])

        return newHKFID

    def removeHotkey(self,hotkey=False, id=False):
        """Remove hotkeys and corresponding function"""
        HKFID = id
        try:
            if hotkey:
                hotkeyList = self.getHotkeyList(hotkey)
                try:
                    UserHKFTemp = [[hotk, fun] for hotk, fun in self.UserHKF if not(hotk in hotkeyList)]
                    self.UserHKF = UserHKFTemp[:]
                except:
                    pass
                try:
                    UserHKFTemp = [[hotk, fun] for hotk, fun in self.UserHKFUp if not(hotk in hotkeyList)]
                    self.UserHKFUp = UserHKFTemp[:]
                except:
                    pass
            elif HKFID:
                for item in self.HKFIDDict[HKFID]:
                    try:
                        self.UserHKF.remove(item)
                    except:
                        self.UserHKFUp.remove(item)
                self.HKFIDDict.pop(HKFID)
            else:
                self.UserHKF = []
                self.UserHKFUp = []
        except:
            pass


    def setEndHotkey(self, hotkey):
        """Add exit hotkeys"""
        self.removeHotkey(self.EndHotkey)
        self.EndHotkey = hotkey
        self.addHotkey(hotkey,self.end)

#--------------------------------------------------------
#ID functions for HKFID
    def getNewHKFID(self):
        try:
            return max(self.HKFIDDict.keys())+1
        except:
            return 1

#--------------------------------------------------------

    def isHotkey(self,hotkey):
        """Check if hotkey is pressed down
            Hotkey is given as KeyID"""

        try:
            #make sure exact hotkey is pressed
            if not(len(hotkey) == len(self.KeyDownID)):
                return  False
            for hotk in hotkey:
                if not(hotk in self.KeyDownID):
                    return False
        except:
            return False

        return True

    def OnKeyDown(self,event):

        if not "mouse" in event.MessageName:
            #check for merged keys first
            eventID = event.KeyID
        else:
            eventID = self.mouseDown_eventMessage2MID[event.Message]

        #make sure key only gets presse once
        if not(eventID in self.KeyDownID):

            self.KeyDownID.append(eventID)

            #Add user hotkeys and functions
            for hk, fun in self.UserHKF:
                if self.isHotkey(hk):
                    fun()

        return True


    def OnKeyUp(self,event):

        if not "mouse" in event.MessageName:
            eventID = event.KeyID
        else:
            eventID = self.mouseUp_eventMessage2MID[event.Message]

        #check for hotkey up keys
        for hk, fun in self.UserHKFUp:
            if hk[0] == eventID:
                fun()


        try:
            self.KeyDownID.remove(eventID)

        except:
            pass
        return True

    def OnSingleEventMouse(self, event):
        """Function to excetue single mouse events"""

        if event.Message in self.singleEventMouseMessage:
            #test for mouse wheel:
            if event.Message == 522:
                if event.Wheel == 1:
                    eventID = 1004
                else:
                    eventID = 1005
            #test mouse move
            elif event.Message == 512:
                eventID = 1000
            else:
                return False

            self.KeyDownID.append(eventID)

            #Add user hotkeys and functions
            for hk, fun in self.UserHKF:
                if self.isHotkey(hk):
                    fun()

            self.KeyDownID.remove(eventID)

        return True


#--------------------------------------------------------

    def createKeyLookup(self):
        """Creates Key look up dictionaries, change names as you please"""
        ID2Key = {   8: 'Back',
                     9: 'Tab',
                     13: 'Return',
                     20: 'Capital',
                     27: 'Escape',
                     32: 'Space',
                     33: 'Prior',
                     34: 'Next',
                     35: 'End',
                     36: 'Home',
                     37: 'Left',
                     38: 'Up',
                     39: 'Right',
                     40: 'Down',
                     44: 'Snapshot',
                     46: 'Delete',
                     48: '0',
                     49: '1',
                     50: '2',
                     51: '3',
                     52: '4',
                     53: '5',
                     54: '6',
                     55: '7',
                     56: '8',
                     57: '9',
                     65: 'A',
                     66: 'B',
                     67: 'C',
                     68: 'D',
                     69: 'E',
                     70: 'F',
                     71: 'G',
                     72: 'H',
                     73: 'I',
                     74: 'J',
                     75: 'K',
                     76: 'L',
                     77: 'M',
                     78: 'N',
                     79: 'O',
                     80: 'P',
                     81: 'Q',
                     82: 'R',
                     83: 'S',
                     84: 'T',
                     85: 'U',
                     86: 'V',
                     87: 'W',
                     88: 'X',
                     89: 'Y',
                     90: 'Z',
                     91: 'Lwin',
                     96: 'Numpad0',
                     97: 'Numpad1',
                     98: 'Numpad2',
                     99: 'Numpad3',
                     100: 'Numpad4',
                     101: 'Numpad5',
                     102: 'Numpad6',
                     103: 'Numpad7',
                     104: 'Numpad8',
                     105: 'Numpad9',
                     106: 'Multiply',
                     107: 'Add',
                     109: 'Subtract',
                     110: 'Decimal',
                     111: 'Divide',
                     112: 'F1',
                     113: 'F2',
                     114: 'F3',
                     115: 'F4',
                     116: 'F5',
                     117: 'F6',
                     118: 'F7',
                     119: 'F8',
                     120: 'F9',
                     121: 'F10',
                     122: 'F11',
                     123: 'F12',
                     144: 'Numlock',
                     160: 'Lshift',
                     161: 'Rshift',
                     162: 'Lcontrol',
                     163: 'Rcontrol',
                     164: 'Lmenu',
                     165: 'Rmenu',
# BEGIN jichi 3/15/2014: Rename these keys
# See: http://msdn.microsoft.com/en-us/library/windows/apps/br211775.aspx
# See: http://www.labdevtools.com/ENG/winform/help/Knob_KeyInput_Keys.html
                     #186: 'Oem_1',
                     #187: 'Oem_Plus',
                     #188: 'Oem_Comma',
                     #189: 'Oem_Minus',
                     #190: 'Oem_Period',
                     #191: 'Oem_2',
                     #192: 'Oem_3',
                     #219: 'Oem_4',
                     #220: 'Oem_5',
                     #221: 'Oem_6',
                     #222: 'Oem_7',
                     186: ';', # semicolon
                     187: '=', # equal
                     188: ',', # comma
                     189: '-', # dash
                     190: '.', # period
                     191: '/', # forwardSlash
                     192: '`', # graveAccent
                     219: '[', # openBracket
                     220: '\\', # backSlash
                     221: ']', # closeBracket
                     222: "'", # singleQuote
# Add more
                     45: 'Insert',
# END jichi 3/15/2014
                     1001: 'mouse left', #mouse hotkeys
                     1002: 'mouse right',
                     1003: 'mouse middle',
                     1000: 'mouse move', #single event hotkeys
                     1004: 'mouse wheel up',
                     1005: 'mouse wheel down',
                     1010: 'Ctrl', #merged hotkeys
                     1011: 'Alt',
                     1012: 'Shift'}

        Key2ID = dict(map(lambda x,y: (x,y),ID2Key.values(),ID2Key.keys()))

        return ID2Key, Key2ID

    def createMouseLookup(self):
        """Takes a event.Message from mouse and converts it to artificial KeyID"""
        mouseDown_MID2eventMessage = {
                                        1001:513,
                                        1002:516,
                                        1003:519}
        mouseDown_eventMessage2MID = dict(map(lambda x,y: (x,y),mouseDown_MID2eventMessage.values(),mouseDown_MID2eventMessage.keys()))

        mouseUp_MID2eventMessage = {
                                        1001:514,
                                        1002:517,
                                        1003:520}
        mouseUp_eventMessage2MID = dict(map(lambda x,y: (x,y),mouseUp_MID2eventMessage.values(),mouseUp_MID2eventMessage.keys()))

        return mouseDown_MID2eventMessage, mouseDown_eventMessage2MID, mouseUp_MID2eventMessage, mouseUp_eventMessage2MID

    def createSingleEventMouse(self):
        """Store events that get executed on single event like wheel up
        MID   event.Message    pyhk hotkey      comments
        1000  512              mouse move
        1004  522              mouse wheel up   event.Wheel = 1
        1005  522              mouse wheel up   event.Wheel = 1"""

        singleEventMouseMessage = [512, 522]
        singleEventMID = [1000,1004,1005]

        return singleEventMouseMessage, singleEventMID

    def createMergeKeys(self):
        """Merge two keys into one
        KeyID   MEID    MergeHumanHotkey
        162     1010     Ctrl  (Lcontrol)
        163     1010     Ctrl  (Rcontrol
        164     1011     Alt   (Lmenu)
        165     1011     Alt   (Rmenu)
        160     1012     Shift (Lshift)
        161     1012     Shift (Rshift)"""

        KeyID2MEID = {162:1010,
                      163:1010,
                      164:1011,
                      165:1011,
                      160:1012,
                      161:1012}
        return KeyID2MEID

    def getHotkeyListNoSingleNoModifiers(self):
        """return a list of all hotkeys without single events and modifiers"""
        TempID2Key = self.ID2Key.copy()

        #get rid of single events and modifiers
        getRid = [160,161,162,163,164,165,1000,1004,1005,1010,1011,1012]

        #get rid of Lwin and oems
        moreRid = [91,186,187,188,189,190,191,192,219,220,221,222]

        for item in moreRid:
            getRid.append(item)

        for gR in getRid:
            TempID2Key.pop(gR)

        LTempID2Key = TempID2Key.values()

        return LTempID2Key


#permutation functions needed for merged hotkeys
def UniquePermutation2(l1,l2):
    """"Return UP of two lists"""
    ltemp =[]
    for x1 in l1:
        for x2 in l2:
            ltemp.append([x1,x2])

    return ltemp

def UniquePermutation(li):
    """Return UP of a general list"""
    lcurrent = li[0]
    depth = 0
    for xl in li[1:]:
        lcurrenttemp = list()
        lcurrenttemp = UniquePermutation2(lcurrent,xl)

        if depth > 0:
            lcurrent = list()
            for item in lcurrenttemp:
                item0 = list(item[0])
                item0.append(item[1])
                lcurrent.append(item0)
        else:
            lcurrent = lcurrenttemp[:]
        depth += 1
    return lcurrent

#class for thread
class ExecFunThread:
    def __init__(self, fun):
        self.fun = fun

    def Start(self):
        self.running = True
        thread.start_new_thread(self.Run, ())

    def IsRunning(self):
        return self.running

    def Run(self):
        self.fun()
        self.running = False

if __name__ == '__main__':

    import win32gui

    #define test function
    def funk1():
        print "Hotkey pressed: Lcontrol 7"

    def funM():
        print "Mousy"

    def funW():
        print "Wheel up"

    def funWin():
        print win32gui.GetForegroundWindow()

    #create pyhk class instance
    hot = pyhk()

    print hot.getHotkeyListNoSingleNoModifiers()

    #add hotkey
    id1 = hot.addHotkey(['Ctrl', 'Alt','7'],funk1)
    id2 = hot.addHotkey(['Ctrl', 'Alt','7'],funk1)
    id3 = hot.addHotkey(['Ctrl', 'Alt','7'],funk1)

    id4 = hot.addHotkey(['7'],funWin,isThread = True, up=True)
    id4 = hot.addHotkey(['7'],funWin,isThread = True, up=True)

    #add hotkey
    hot.addHotkey(['mouse middle','1'],funM)

    #add hotkey
    hot.addHotkey(["mouse wheel up"],funW)

    #start looking for hotkey. Loop is ended with CTRL Shift Q
    hot.start()

    #add several hotkeys to the same function
    hot.addHotkey(['Ctrl','8'],funk1)

    #remove hotkey Lcontrol 7
    hot.removeHotkey(id = id4)

    #change exit hotkey,
    #default is CTRL Shift Q and will be used if no EndHotkey is set
    hot.setEndHotkey(['Alt','Q'])

    #start looking for hotkey
    hot.start()




