ComplexTools/KeyboardMapper.py

ComplexTools/KeyboardMapper.py
1 # Copyright 2009 Autodesk, Inc. All rights reserved.
2 # Use of this software is subject to the terms of the Autodesk license agreement
3 # provided at the time of installation or download, or which otherwise accompanies
4 # this software in either electronic or hard copy form.
5 #
6 # Script description:
7 # A tool that allows editing of MoBu keyboard mapping files.
8 #
9 # Topic: FBInputType, FBLayout, FBConfigFile, FBSpread
10 #
11 
12 from pyfbsdk import *
13 from pyfbsdk_additions import *
14 
15 import os
16 import os.path
17 import ConfigParser
18 import mbutils
19 
20 # Methods to display a shortcut to the user
21 def UIValue(key, mods):
22  if key:
23  return "%s : %s" % (mods, key)
24  else:
25  return ""
26 
27 # class to store anything about a keyboard mapping (action and key used)
28 class Mapping(object):
29  def __init__(self,action,key="", mods="", time = "DN"):
30  self.action = action
31  self.mods = self.edit_mods = mods
32  self.key = self.edit_key = key
33  self.time = time
34 
35  # Format the shortcut the way MB expects it.
36  def FileValue(self):
37  if self.key:
38  return "{%s:%s*%s}" % (self.mods,self.key,self.time)
39  else:
40  return ""
41 
42  # Format the shorcut for ui presentation
43  def UIValue(self):
44  return UIValue(self.key,self.mods)
45 
46 # Create a MB shortcut description that can contains multiple mappings
47 def MappingsToShortcut(mappings):
48  shortcut = ""
49  for i, mapping in enumerate(mappings):
50  value = mapping.FileValue()
51  if value:
52  if shortcut:
53  shortcut += "|"
54  shortcut += value
55  return shortcut
56 
57 MOD_TO_ID = { "NONE" : 0,"SHFT" : 1,"CTRL" : 2,"ALT" : 4,"ALCT" : 6,"ALSH" : 5,"CTSH" : 3,"ALCTSH" : 7 }
58 
59 KEY_TO_ID = { "NONE" : -1,
60 "ESC" : 0x1b, "TAB" : 0x09, "CAPS" : 0x14, "BKSP" : 0x08, "LBR" : 0xdb, "RBR" : 0xdd, "SEMI" : 0xba, "ENTR" : 0x0d,
61 "SPC" : 0x20, "PRNT" : 0x2c, "SCRL" : 0x91, "PAUS" : 0x13, "INS" : 0x2d, "HOME" : 0x24, "PGUP" : 0x21, "DEL" : 0x2e,
62 "END" : 0x1b, "PGDN" : 0x1b, "UP" : 0x1b, "LEFT" : 0x1b, "DOWN" : 0x1b, "RGHT" : 0x1b,
63 "F1" : 0x70,"F2" : 0x71 ,"F3" : 0x72, "F4" : 0x73, "F5" : 0x74, "F6" : 0x75, "F7" : 0x76, "F8" : 0x77, "F9" : 0x78,"F10" : 0x79 ,"F11" : 0x7a, "F12" : 0x7b,
64 "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,
65 "'" : 0xde, "," : 0xbc, "-" : 0xbd, "/" : 0xbf,"=" : 0xbb ,"." : 0xbe, "\\" : 0xdc, "`" : 0xc0,
66 "0" : 48, "1" : 49, "2" : 50, "3" : 51, "4" : 52,"5" : 53 ,"6" : 54, "7" : 55, "8" : 56, "9" : 59,
67 "A" : 65, "B" : 66,"C" : 67, "D" : 68, "E" : 69,"F" : 70 , "G" : 71, "H" : 72,"I" : 73,
68 "J" : 74, "K" : 75,"L" : 76, "M" : 77, "N" : 78,"O" : 79, "P" : 80, "Q" : 81,"R" : 82 ,
69 "S" : 83, "T" : 84,"U" : 85, "V" : 86, "W" : 87,"X" : 88, "Y" : 89, "Z" : 90
70  }
71 
72 ID_TO_KEY = {}
73 for key, i in KEY_TO_ID.iteritems():
74  ID_TO_KEY[i] = key
75 
76 ID_TO_MOD = {}
77 for key, i in MOD_TO_ID.iteritems():
78  ID_TO_MOD[i] = key
79 
80 class KeyboardMapper(object):
81 
82  def GetMappingFromShortcut(self,key,mods,mappings = None):
83  # Finds the mapping corresponding to key and mods in a mappings list
84  if not mappings:
85  mappings = self.row_to_mapping
86  for mapping in mappings:
87  if key and key == mapping.key and mapping.mods == mods:
88  return mapping
89  return None
90 
91  def OnShortcut(self,control,event):
92  # User has typed a shortcut
93  if event.InputType == FBInputType.kFBKeyPressRaw and event.Key != -1 and self.edit_mapping:
94  # Update edited mapping
95  self.edit_mapping.edit_key = ID_TO_KEY[event.Key]
96  self.edit_mapping.edit_mods = ID_TO_MOD[event.KeyState]
97  # Update UI
98  shortcut = UIValue(self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
99  self.shortcut_edit.Text = shortcut
100  # check if we are in conflict with someone
101  self.conflict_mapping = self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
102  if self.conflict_mapping:
103  self.conflict_edit.Text = self.conflict_mapping.action
104  else:
105  self.conflict_edit.Text = ""
106 
107  def UpdateEditMapping(self,row):
108  # Populate edition bar and edit_mapping according to chosen row
109  if self.shortcut_spread.GetRow(row).RowSelected:
110  mapping = self.row_to_mapping[row]
111  self.action_edit.Text = mapping.action
112  self.shortcut_edit.Text = mapping.UIValue()
113  self.edit_mapping = mapping
114 
115  global foin
116  foin = mapping
117  else:
118  print "none"
119  self.edit_mapping = None
120  self.action_edit.Text = ""
121  self.shortcut_edit.Text = ""
122  self.conflict_mapping = None
123  self.conflict_edit.Text = ""
124 
125 
126  def RowClicked(self,control,event):
127  # User has clicked on a row
128  self.UpdateEditMapping(event.Row)
129 
130  def OnKeyboardChange(self,control,event):
131  # User has changed the keyboard file to edit
132 
133  # Reset all relevant values since we will repopulate everything
134  if self.row_to_mapping:
135  self.shortcut_spread.Clear()
136  self.shortcut_spread.ColumnAdd("Shortcut")
137  self.mapping_to_row = {}
138  self.row_to_mapping = []
139  self.action_to_mappings = {}
140 
141  # Populate our UI with relevant infos from file
142  config = mbutils.OpenConfigFile(self.keyboard_files[self.file_list.ItemIndex])
143  rowref = 0
144 
145  # Sort element alphabetically
146  items = config.items("Actions")
147  items.sort(key = lambda pair : pair[0] )
148 
149  for action, shortcut in items:
150  mappings = self.ParseShortcut(action, shortcut)
151  # Populate spread
152  for i, mapping in enumerate(mappings):
153  if i > 0:
154  self.shortcut_spread.RowAdd("", rowref)
155  else:
156  self.shortcut_spread.RowAdd(action, rowref)
157 
158  # Populate mapping structures
159  self.row_to_mapping.append(mapping)
160  self.mapping_to_row[mapping] = rowref
161  self.shortcut_spread.SetCellValue(rowref, 0, mapping.UIValue())
162  rowref += 1
163 
164 
165  def ParseShortcut(self,action, desc_string):
166  # Parse an action shortcut and populate mapping structures
167  l = []
168  desclist = desc_string.split("|")
169  for desc in desclist:
170  if desc:
171  keystr = desc.strip("{}")
172  mod_keytime = keystr.split(":")
173  key_time = mod_keytime[1].split("*")
174  mapping = Mapping(action,key_time[0],mod_keytime[0],key_time[1])
175  else:
176  mapping = Mapping(action)
177  l.append(mapping)
178  self.action_to_mappings[action] = l
179  return l
180 
181  def Assign(self,control,event):
182  # User wants to change the binding for a particular action
183  if not self.edit_mapping:
184  return
185 
186  action_mappings = self.action_to_mappings[self.edit_mapping.action]
187  # try to add a mapping that exists already for the action
188  if self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods,action_mappings):
189  return
190 
191  # update new mapping
192  self.edit_mapping.key = self.edit_mapping.edit_key
193  self.edit_mapping.mods = self.edit_mapping.edit_mods
194 
195  # Update UI
196  self.shortcut_spread.SetCellValue(self.mapping_to_row[self.edit_mapping], 0, self.edit_mapping.UIValue())
197 
198  # Write the change to file
199  self.WriteEditMapping()
200 
201  def WriteEditMapping(self,update_conflict = True):
202  # Update the edited action
203  keyboard_dir, keyboard_file = os.path.split(self.keyboard_files[self.file_list.ItemIndex])
204  config = FBConfigFile(keyboard_file,keyboard_dir)
205  config.Set("Actions",self.edit_mapping.action,MappingsToShortcut(self.action_to_mappings[self.edit_mapping.action]))
206 
207  # If there is a conflicting binding: unassigned it
208  if self.conflict_mapping:
209  self.conflict_mapping.key = ""
210  self.conflict_mapping.mods = ""
211  if update_conflict:
212  self.shortcut_spread.SetCellValue(self.mapping_to_row[self.conflict_mapping], 0, "")
213  config.Set("Actions",self.conflict_mapping.action,MappingsToShortcut(self.action_to_mappings[self.conflict_mapping.action]))
214  self.conflict_edit.Text = ""
215 
216  def Remove(self,control,event):
217  # User wants to remove a mapping for a particular action
218  if not self.edit_mapping:
219  return
220  row = self.mapping_to_row[self.edit_mapping]
221 
222  # Unbind mapping
223  self.edit_mapping.key = ""
224  self.edit_mapping.mods = ""
225  self.shortcut_edit.Text = ""
226  # Write the change to file
227  self.WriteEditMapping(False)
228 
229  # Reload file and repopulate UI
230  self.OnKeyboardChange(None, None)
231  # set the selection where it was
232  if row > len(self.row_to_mapping):
233  row = len(self.row_to_mapping) - 1
234  self.shortcut_spread.GetRow(row).RowSelected = True
235  # Update the edit box according to selection
236  self.UpdateEditMapping(row)
237 
238 
239  def Add(self,control,event):
240  # User wants to add a new binding for this action
241  if not self.edit_mapping:
242  return
243 
244  action_mappings = self.action_to_mappings[self.edit_mapping.action]
245 
246  # try to add a mapping that exists already for the action
247  if self.GetMappingFromShortcut(self.edit_mapping.edit_key,self.edit_mapping.edit_mods,action_mappings):
248  return
249 
250  row = self.mapping_to_row[action_mappings[0]] + len(action_mappings)
251 
252  # Create new mapping and add it so its gets written to file
253  mapping = Mapping(self.edit_mapping.action,self.edit_mapping.edit_key,self.edit_mapping.edit_mods)
254  self.edit_mapping = mapping
255  action_mappings.append(mapping)
256  self.WriteEditMapping(True)
257 
258  # Reload file and repopulate UI
259  self.OnKeyboardChange(None, None)
260  # set the selection where it was
261  if row > len(self.row_to_mapping):
262  row = len(self.row_to_mapping) - 1
263  self.shortcut_spread.GetRow(row).RowSelected = True
264  # Update the edit box according to selection
265  self.UpdateEditMapping(row)
266 
267  def Show(self):
268  self.popup.Show()
269  del self.popup
270 
271  def ClosePopup(self,control, event):
272  self.popup.Close(True)
273 
274  def __init__(self):
275  # Create the Keyboard Mapper in a Modal Popup. this way no shortcut are sent to the application
276  # when we assign new shortcut to actions
277  self.popup = FBPopup()
278 
279  self.popup.Caption = "Keyboard Mapper"
280  self.popup.Modal = True
281  x = FBAddRegionParam(0,FBAttachType.kFBAttachLeft,"")
282  y = FBAddRegionParam(0,FBAttachType.kFBAttachTop,"")
283  w = FBAddRegionParam(0,FBAttachType.kFBAttachRight,"")
284  h = FBAddRegionParam(0,FBAttachType.kFBAttachBottom,"")
285  self.popup.AddRegion("main","main", x,y,w,h)
286  self.popup.Left = 300
287  self.popup.Top = 300
288  self.popup.Width = 650
289  self.popup.Height = 500
290 
291  grid = FBGridLayout()
292  self.popup.SetControl("main",grid)
293 
294  row = 0
295  widgetHeight = 25
296  buttonWidth = 90
297 
298  # Row 1
299  l = FBLabel()
300  l.Caption = "Keyboard files"
301  grid.Add(l, row, 0)
302  grid.SetRowHeight(row, widgetHeight)
303  row += 1
304 
305  # Row 2: keyboard file list
306  self.file_list = FBList()
307  self.file_list.OnChange.Add(self.OnKeyboardChange)
308  self.file_list.Style = FBListStyle.kFBDropDownList
309  grid.Add(self.file_list, row, 0)
310  grid.SetRowHeight(row, widgetHeight)
311  row += 1
312 
313  # Row 3
314  l = FBLabel()
315  l.Caption = "Shortcut list"
316  grid.Add(l, row, 0)
317  grid.SetRowHeight(row, widgetHeight)
318  row += 1
319 
320  # row 4
321  self.shortcut_spread = FBSpread()
322  self.shortcut_spread.Caption = "Action Name"
323  self.shortcut_spread.ColumnAdd("Shortcut")
324  self.shortcut_spread.OnRowClick.Add(self.RowClicked)
325  grid.AddRange(self.shortcut_spread, row, row, 0, 2)
326  grid.SetRowRatio(row, 1.0)
327  row += 1
328 
329  # Row 5
330  l = FBLabel()
331  l.Caption = "Action Name"
332  grid.Add(l, row, 0)
333 
334  l = FBLabel()
335  l.Caption = "Shortcut"
336  grid.Add(l, row, 2)
337 
338  grid.SetRowHeight(row, widgetHeight)
339  row += 1
340 
341  # Row 6
342  self.action_edit = FBEdit()
343  self.action_edit.ReadOnly = True
344  grid.Add(self.action_edit, row, 0)
345 
346  # Trick: FBLayout is the only class with a OnInput event. So if we put
347  # focus in this layout and if we type something on the keyboard we "catch" the
348  # shortcut that has been typed.
349  self.input_layout = FBLayout()
350  x = FBAddRegionParam(5,FBAttachType.kFBAttachLeft,"")
351  y = FBAddRegionParam(5,FBAttachType.kFBAttachTop,"")
352  w = FBAddRegionParam(-5,FBAttachType.kFBAttachRight,"")
353  h = FBAddRegionParam(-5,FBAttachType.kFBAttachBottom,"")
354  self.input_layout.AddRegion("Border","Click here and type a shortcut", x, y, w, h)
355  self.input_layout.SetBorder("Border",FBBorderStyle.kFBEmbossBorder,True, False,2,2,90,0)
356  self.input_layout.OnInput.Add(self.OnShortcut)
357  grid.Add(self.input_layout, row, 1)
358 
359  self.shortcut_edit = FBEdit()
360  self.shortcut_edit.ReadOnly = True
361  grid.Add(self.shortcut_edit, row, 2)
362  grid.SetRowHeight(row, widgetHeight)
363  row += 1
364 
365  hbox = FBHBoxLayout()
366  b = FBButton()
367  b.Caption = "Replace shortcut"
368  b.OnClick.Add(self.Assign)
369  hbox.Add(b, buttonWidth)
370  b = FBButton()
371  b.Caption = "Add shortcut"
372  b.OnClick.Add(self.Add)
373  hbox.Add(b, buttonWidth)
374  b = FBButton()
375  b.Caption = "Remove shortcut"
376  b.OnClick.Add(self.Remove)
377  hbox.Add(b, buttonWidth)
378 
379  grid.AddRange(hbox, row, row, 0, 1)
380  grid.SetRowHeight(row, widgetHeight)
381  row += 1
382 
383  # Row 7
384  l = FBLabel()
385  l.Caption = "Shortcut already assigned to action"
386  grid.AddRange(l, row, row, 0,2)
387  grid.SetRowHeight(row, widgetHeight)
388  row += 1
389 
390  # Row 8
391  self.conflict_edit = FBEdit()
392  self.conflict_edit.ReadOnly = True
393  grid.Add(self.conflict_edit, row, 0)
394  grid.SetRowHeight(row, widgetHeight)
395  row += 1
396 
397  # Row 9
398  hbox = FBHBoxLayout()
399  b = FBButton()
400  b.Caption = "Close"
401  b.OnClick.Add(self.ClosePopup)
402  hbox.Add(b, buttonWidth)
403  l = FBLabel()
404  l.Caption = 'Reset Settings->Keyboard configuration->"Your Keyboard" for changes to take place.'
405  l.Style = FBTextStyle.kFBTextStyleBold
406  hbox.Add(l, 600)
407 
408  grid.AddRange(hbox, row, row, 0, 2)
409  grid.SetRowHeight(row, widgetHeight)
410  row += 1
411 
412  # Init data structure:
413  self.action_to_mappings = {}
414  self.mapping_to_row = {}
415  self.row_to_mapping = []
416  self.edit_mapping = None
417  self.conflict_mapping = None
418 
419  self.keyboard_files = []
420  # Populate with Default Keyboard files:
421  keyboard_folder = os.path.join(mbutils.GetConfigPath(),"Keyboard")
422  for f in os.listdir(keyboard_folder):
423  self.keyboard_files.append(os.path.join(keyboard_folder,f))
424  self.file_list.Items.append(f)
425 
426  PythonKeyboardPath = os.path.join(mbutils.GetUserConfigPath(),"Python", "PythonKeyboard.txt")
427 
428  if os.path.isfile(PythonKeyboardPath):
429  # Add User Python Console keyboard mapping
430  self.keyboard_files.append(os.path.join(mbutils.GetUserConfigPath(),"Python", "PythonKeyboard.txt"))
431  self.file_list.Items.append("PythonKeyboard.txt")
432  else:
433  # Add Default Python Console keyboard mapping
434  self.keyboard_files.append(os.path.join(mbutils.GetConfigPath(),"Python\Keyboard", "PythonKeyboard.txt"))
435  self.file_list.Items.append("PythonKeyboard.txt")
436 
437  self.OnKeyboardChange(None, None)
438 
439 def PopKeyboardMapper(control,event):
440  mapper = KeyboardMapper()
441  mapper.Show()
442  del mapper
443 
444 def PopulateLayout(tool):
445  # Keyboard mapper is created as a popup inside a Tool to avoid this behavior:
446  # When we execute a script we automatically changed the cursor to a wair cursor
447  # for the whole script execution.
448  # Problem is: if executing a script create a popup, while the dialog is up the cursor
449  # is a wait cursor since the script has not technically finished its execution!
450  # if we start the Mapper from a tool, the script ends its execution when the tool is
451  # executed and starting the KeyboardMapper popup keep the normal cursor.
452 
453  x = FBAddRegionParam(0,FBAttachType.kFBAttachLeft,"")
454  y = FBAddRegionParam(0,FBAttachType.kFBAttachTop,"")
455  w = FBAddRegionParam(0,FBAttachType.kFBAttachRight,"")
456  h = FBAddRegionParam(0,FBAttachType.kFBAttachBottom,"")
457 
458  vbox = FBVBoxLayout()
459  tool.AddRegion("main","main", x, y, w, h)
460  tool.SetControl("main",vbox)
461 
462  b = FBButton()
463  b.Caption = "Keyboard mapper"
464  vbox.Add(b, 35)
465  b.OnClick.Add(PopKeyboardMapper)
466 
467 def CreateTool():
468  tool = FBCreateUniqueTool("Keyboard Utilities")
469  tool.StartSizeX = 200
470  tool.StartSizeY = 100
471  PopulateLayout(tool)
472  ShowTool(tool)
473 
474 
475 CreateTool()
476