# Copyright 2009 Autodesk, Inc.  All rights reserved.
# Use of this software is subject to the terms of the Autodesk license agreement 
# provided at the time of installation or download, or which otherwise accompanies
# this software in either electronic or hard copy form.
#
# Script description:
# A tool that allows editing of MoBu keyboard mapping files.
#
# Topic: FBInputType, FBLayout, FBConfigFile, FBSpread
# 

from pyfbsdk import *
from pyfbsdk_additions import *

import os
import os.path
import ConfigParser
import mbutils

# Methods to display a shortcut to the user
def UIValue(key, mods):
    if key:
        return "%s : %s" % (mods, key)
    else:
        return ""

# class to store anything about a keyboard mapping (action and key used)
class Mapping(object):
    def __init__(self,action,key="", mods="", time = "DN"):
        self.action = action
        self.mods = self.edit_mods = mods
        self.key = self.edit_key = key
        self.time = time

    # Format the shortcut the way MB expects it.
    def FileValue(self):
        if self.key:
            return "{%s:%s*%s}" % (self.mods,self.key,self.time)
        else:
            return ""

    # Format the shorcut for ui presentation
    def UIValue(self):
        return UIValue(self.key,self.mods)

# Create a MB shortcut description that can contains multiple mappings
def MappingsToShortcut(mappings):
    shortcut = ""
    for i, mapping in enumerate(mappings):
        value = mapping.FileValue()
        if value:
            if shortcut:
                shortcut += "|"
            shortcut += value
    return shortcut

MOD_TO_ID = { "NONE" : 0,"SHFT" : 1,"CTRL" : 2,"ALT" : 4,"ALCT" : 6,"ALSH" : 5,"CTSH" : 3,"ALCTSH" : 7 }

KEY_TO_ID = { "NONE" : -1,
"ESC" : 0x1b, "TAB" : 0x09, "CAPS" : 0x14, "BKSP" : 0x08, "LBR" : 0xdb, "RBR" : 0xdd, "SEMI" : 0xba, "ENTR" : 0x0d,
"SPC" : 0x20, "PRNT" : 0x2c, "SCRL" : 0x91, "PAUS" : 0x13, "INS" : 0x2d, "HOME" : 0x24, "PGUP" : 0x21, "DEL" : 0x2e,
"END" : 0x1b, "PGDN" : 0x1b, "UP" : 0x1b, "LEFT" : 0x1b, "DOWN" : 0x1b, "RGHT" : 0x1b,
"F1" : 0x70,"F2" : 0x71 ,"F3" : 0x72, "F4" : 0x73, "F5" : 0x74, "F6" : 0x75, "F7" : 0x76, "F8" : 0x77, "F9" : 0x78,"F10" : 0x79 ,"F11" : 0x7a, "F12" : 0x7b,
"NUML" : 0x90, "NMUL" : 0x6a, "NADD" : 0x6b, "NDIV" : 0x6f, "NSUB" : 0x6d,"NDEC" : 0x6e ,"N0" : 0x60, "N1" : 0x61, "N2" : 0x62, "N3" : 0x63, "N4" : 0x64, "N5" : 0x65, "N6" : 0x66,"N7" : 0x67 ,"N8" : 0x68, "N9" : 0x69,
"'" : 0xde, "," : 0xbc, "-" : 0xbd, "/" : 0xbf,"=" : 0xbb ,"." : 0xbe, "\\" : 0xdc, "`" : 0xc0,
"0" : 48, "1" : 49, "2" : 50, "3" : 51, "4" : 52,"5" : 53 ,"6" : 54, "7" : 55, "8" : 56, "9" : 59,
"A" : 65, "B" : 66,"C" : 67, "D" : 68, "E" : 69,"F" : 70 , "G" : 71, "H" : 72,"I" : 73,
"J" : 74, "K" : 75,"L" : 76, "M" : 77, "N" : 78,"O" : 79, "P" : 80, "Q" : 81,"R" : 82 ,
"S" : 83, "T" : 84,"U" : 85, "V" : 86, "W" : 87,"X" : 88, "Y" : 89, "Z" : 90
 }

ID_TO_KEY = {}
for key, i in KEY_TO_ID.iteritems():
    ID_TO_KEY[i] = key

ID_TO_MOD = {}
for key, i in MOD_TO_ID.iteritems():
    ID_TO_MOD[i] = key

class KeyboardMapper(object):

    def GetMappingFromShortcut(self,key,mods,mappings = None):
        # Finds the mapping corresponding to key and mods in a mappings list
        if not mappings:
            mappings = self.row_to_mapping
        for mapping in mappings:
            if key and key == mapping.key and mapping.mods == mods:
                return mapping
        return None

    def OnShortcut(self,control,event):
        # User has typed a shortcut
        if event.InputType == FBInputType.kFBKeyPressRaw and event.Key != -1 and self.edit_mapping:
            # Update edited mapping
            self.edit_mapping.edit_key = ID_TO_KEY[event.Key]
            self.edit_mapping.edit_mods = ID_TO_MOD[event.KeyState]
            # Update UI
            shortcut = UIValue(self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
            self.shortcut_edit.Text = shortcut
            # check if we are in conflict with someone
            self.conflict_mapping = self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
            if self.conflict_mapping:
                self.conflict_edit.Text = self.conflict_mapping.action
            else:
                self.conflict_edit.Text = ""

    def UpdateEditMapping(self,row):
        print "selcted", row
        # Populate edition bar and edit_mapping according to chosen row
        if self.shortcut_spread.GetRow(row).RowSelected:
            mapping = self.row_to_mapping[row]
            self.action_edit.Text = mapping.action
            self.shortcut_edit.Text = mapping.UIValue()
            self.edit_mapping = mapping

            global foin
            foin = mapping
        else:
            print "none"
            self.edit_mapping = None
            self.action_edit.Text = ""
            self.shortcut_edit.Text = ""
        self.conflict_mapping = None
        self.conflict_edit.Text = ""


    def RowClicked(self,control,event):
        # User has clicked on a row
        self.UpdateEditMapping(event.Row)

    def OnKeyboardChange(self,control,event):
        # User has changed the keyboard file to edit

        # Reset all relevant values since we will repopulate everything
        if self.row_to_mapping:
            self.shortcut_spread.Clear()
            self.shortcut_spread.ColumnAdd("Shortcut")
            self.mapping_to_row = {}
            self.row_to_mapping = []
            self.action_to_mappings = {}

        # Populate our UI with relevant infos from file
        config = mbutils.OpenConfigFile(self.keyboard_files[self.file_list.ItemIndex])
        rowref = 0

        # Sort element alphabetically
        items = config.items("Actions")
        items.sort(key = lambda pair : pair[0] )

        for action, shortcut in items:
            mappings = self.ParseShortcut(action, shortcut)
            # Populate spread
            for i, mapping in enumerate(mappings):
                if i > 0:
                    self.shortcut_spread.RowAdd("", rowref)
                else:
                    self.shortcut_spread.RowAdd(action, rowref)

                # Populate mapping structures
                self.row_to_mapping.append(mapping)
                self.mapping_to_row[mapping] = rowref
                self.shortcut_spread.SetCellValue(rowref, 0, mapping.UIValue())
                rowref += 1


    def ParseShortcut(self,action, desc_string):
        # Parse an action shortcut and populate mapping structures
        l = []
        desclist = desc_string.split("|")
        for desc in desclist:
            if desc:
                keystr = desc.strip("{}")
                mod_keytime = keystr.split(":")
                key_time = mod_keytime[1].split("*")
                mapping = Mapping(action,key_time[0],mod_keytime[0],key_time[1])
            else:
                mapping = Mapping(action)
            l.append(mapping)
        self.action_to_mappings[action] = l
        return l

    def Assign(self,control,event):
        # User wants to change the binding for a particular action
        if not self.edit_mapping:
            return

        action_mappings = self.action_to_mappings[self.edit_mapping.action]
        # try to add a mapping that exists already for the action
        if self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods,action_mappings):
            return

        # update new mapping  
        self.edit_mapping.key = self.edit_mapping.edit_key
        self.edit_mapping.mods = self.edit_mapping.edit_mods

        # Update UI
        self.shortcut_spread.SetCellValue(self.mapping_to_row[self.edit_mapping], 0, self.edit_mapping.UIValue())

        # Write the change to file
        self.WriteEditMapping()

    def WriteEditMapping(self,update_conflict = True):
        # Update the edited action
        keyboard_dir, keyboard_file = os.path.split(self.keyboard_files[self.file_list.ItemIndex])
        config = FBConfigFile(keyboard_file,keyboard_dir)
        config.Set("Actions",self.edit_mapping.action,MappingsToShortcut(self.action_to_mappings[self.edit_mapping.action]))

        # If there is a conflicting binding: unassigned it
        if self.conflict_mapping:
            self.conflict_mapping.key = ""
            self.conflict_mapping.mods = ""
            if update_conflict:
                self.shortcut_spread.SetCellValue(self.mapping_to_row[self.conflict_mapping], 0, "")
                config.Set("Actions",self.conflict_mapping.action,MappingsToShortcut(self.action_to_mappings[self.conflict_mapping.action]))
            self.conflict_edit.Text = ""

    def Remove(self,control,event):
        # User wants to remove a mapping for a particular action
        if not self.edit_mapping:
            return
        row = self.mapping_to_row[self.edit_mapping]

        # Unbind mapping
        self.edit_mapping.key = ""
        self.edit_mapping.mods = ""
        self.shortcut_edit.Text = ""
        # Write the change to file
        self.WriteEditMapping(False)

        # Reload file and repopulate UI
        self.OnKeyboardChange(None, None)
        # set the selection where it was
        if row > len(self.row_to_mapping):
            row = len(self.row_to_mapping) - 1
        self.shortcut_spread.GetRow(row).RowSelected = True
        # Update the edit box according to selection
        self.UpdateEditMapping(row)


    def Add(self,control,event):
        # User wants to add a new binding for this action        
        if not self.edit_mapping:
            return

        action_mappings = self.action_to_mappings[self.edit_mapping.action]

        # try to add a mapping that exists already for the action
        if self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods,action_mappings):
            return

        row = self.mapping_to_row[action_mappings[0]] + len(action_mappings)

        # Create new mapping and add it so its gets written to file
        mapping = Mapping(self.edit_mapping.action,self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
        self.edit_mapping = mapping
        action_mappings.append(mapping)
        self.WriteEditMapping(True)

        # Reload file and repopulate UI
        self.OnKeyboardChange(None, None)
        # set the selection where it was
        if row > len(self.row_to_mapping):
            row = len(self.row_to_mapping) - 1
        self.shortcut_spread.GetRow(row).RowSelected = True
        # Update the edit box according to selection
        self.UpdateEditMapping(row)

    def Show(self):
        self.popup.Show()
        del self.popup

    def ClosePopup(self,control, event):
        self.popup.Close(True)

    def __init__(self):
        # Create the Keyboard Mapper in a Modal Popup. this way no shortcut are sent to the application
        # when we assign new shortcut to actions
        self.popup = FBPopup()

        self.popup.Caption = "Keyboard Mapper"
        self.popup.Modal = True
        x = FBAddRegionParam(0,FBAttachType.kFBAttachLeft,"")
        y = FBAddRegionParam(0,FBAttachType.kFBAttachTop,"")
        w = FBAddRegionParam(0,FBAttachType.kFBAttachRight,"")
        h = FBAddRegionParam(0,FBAttachType.kFBAttachBottom,"")
        self.popup.AddRegion("main","main", x,y,w,h)
        self.popup.Left = 300
        self.popup.Top = 300
        self.popup.Width = 650
        self.popup.Height = 500

        grid = GridLayout()
        self.popup.SetControl("main",grid)

        row = 0
        widgetHeight = 25
        buttonWidth = 90

        # Row 1
        l = FBLabel()
        l.Caption = "Keyboard files"
        grid.Add(l, row, 0)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 2: keyboard file list
        self.file_list = FBList()
        self.file_list.OnChange.Add(self.OnKeyboardChange)
        self.file_list.Style = FBListStyle.kFBDropDownList
        grid.Add(self.file_list, row, 0)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 3
        l = FBLabel()
        l.Caption = "Shortcut list"
        grid.Add(l, row, 0)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # row 4
        self.shortcut_spread = FBSpread()
        self.shortcut_spread.Caption = "Action Name"
        self.shortcut_spread.ColumnAdd("Shortcut")
        self.shortcut_spread.OnRowClick.Add(self.RowClicked)
        grid.AddRange(self.shortcut_spread, row, row, 0, 2)
        grid.SetRowRatio(row, 1.0)
        row += 1

        # Row 5
        l = FBLabel()
        l.Caption = "Action Name"
        grid.Add(l, row, 0)

        l = FBLabel()
        l.Caption = "Shortcut"
        grid.Add(l, row, 2)

        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 6
        self.action_edit = FBEdit()
        self.action_edit.ReadOnly = True
        grid.Add(self.action_edit, row, 0)

        # Trick: FBLayout is the only class with a OnInput event. So if we put
        # focus in this layout and if we type something on the keyboard we "catch" the 
        # shortcut that has been typed.
        self.input_layout = FBLayout()
        x = FBAddRegionParam(5,FBAttachType.kFBAttachLeft,"")
        y = FBAddRegionParam(5,FBAttachType.kFBAttachTop,"")
        w = FBAddRegionParam(-5,FBAttachType.kFBAttachRight,"")
        h = FBAddRegionParam(-5,FBAttachType.kFBAttachBottom,"")
        self.input_layout.AddRegion("Border","Click here and type a shortcut", x, y, w, h)
        self.input_layout.SetBorder("Border",FBBorderStyle.kFBEmbossBorder,True, False,2,2,90,0)
        self.input_layout.OnInput.Add(self.OnShortcut)
        grid.Add(self.input_layout, row, 1)

        self.shortcut_edit = FBEdit()
        self.shortcut_edit.ReadOnly = True
        grid.Add(self.shortcut_edit, row, 2)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        hbox = HBoxLayout()
        b = FBButton()
        b.Caption = "Replace shortcut"
        b.OnClick.Add(self.Assign)
        hbox.Add(b, buttonWidth)
        b = FBButton()
        b.Caption = "Add shortcut"
        b.OnClick.Add(self.Add)
        hbox.Add(b, buttonWidth)
        b = FBButton()
        b.Caption = "Remove shortcut"
        b.OnClick.Add(self.Remove)
        hbox.Add(b, buttonWidth)

        grid.AddRange(hbox, row, row, 0, 1)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 7
        l = FBLabel()
        l.Caption = "Shortcut already assigned to action"
        grid.AddRange(l, row, row, 0,2)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 8
        self.conflict_edit = FBEdit()
        self.conflict_edit.ReadOnly = True
        grid.Add(self.conflict_edit, row, 0)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Row 9
        hbox = HBoxLayout()
        b = FBButton()
        b.Caption = "Close"
        b.OnClick.Add(self.ClosePopup)
        hbox.Add(b, buttonWidth)
        l = FBLabel()
        l.Caption = 'Reset Settings->Keyboard configuration->"Your Keyboard" for changes to take place.'
        l.Style = FBTextStyle.kFBTextStyleBold
        hbox.Add(l, 600)

        grid.AddRange(hbox, row, row, 0, 2)
        grid.SetRowHeight(row, widgetHeight)
        row += 1

        # Init data structure:
        self.action_to_mappings = {}
        self.mapping_to_row = {}
        self.row_to_mapping = []
        self.edit_mapping = None
        self.conflict_mapping = None

        self.keyboard_files = []
        # Populate with Keyboard files:
        keyboard_folder = os.path.join(mbutils.GetConfigPath(),"Keyboard")
        for f in os.listdir(keyboard_folder):
            self.keyboard_files.append(os.path.join(keyboard_folder,f))
            self.file_list.Items.append(f)
        # Add Python Console keyboard mapping
        self.keyboard_files.append(os.path.join(mbutils.GetConfigPath(),"Python", "PythonKeyboard.txt"))
        self.file_list.Items.append("PythonKeyboard.txt")
        self.OnKeyboardChange(None, None)

def PopKeyboardMapper(control,event):
    mapper = KeyboardMapper()
    mapper.Show()
    del mapper

def PopulateLayout(tool):
    # Keyboard mapper is created as a popup inside a Tool to avoid this behavior:
    # When we execute a script we automatically changed the cursor to a wair cursor
    # for the whole script execution. 
    # Problem is: if executing a script create a popup, while the dialog is up the cursor
    # is a wait cursor since the script has not technically finished its execution!
    # if we start the Mapper from a tool, the script ends its execution when the tool is
    # executed and starting the KeyboardMapper popup keep the normal cursor.

    x = FBAddRegionParam(0,FBAttachType.kFBAttachLeft,"")
    y = FBAddRegionParam(0,FBAttachType.kFBAttachTop,"")
    w = FBAddRegionParam(0,FBAttachType.kFBAttachRight,"")
    h = FBAddRegionParam(0,FBAttachType.kFBAttachBottom,"")

    vbox = VBoxLayout()
    tool.AddRegion("main","main", x, y, w, h)
    tool.SetControl("main",vbox)

    b = FBButton()
    b.Caption = "Keyboard mapper"
    vbox.Add(b, 35)
    b.OnClick.Add(PopKeyboardMapper)

def CreateTool():
    tool = CreateUniqueTool("Keyboard Utilities")
    tool.StartSizeX = 200
    tool.StartSizeY = 100
    PopulateLayout(tool)
    ShowTool(tool)


CreateTool()