Assign action to keyboard shortcut through script


#29

:hmm: when you execute the command

actionman.loadkeyboardfile()

it fixes the indices in the .kbd file so i dont think its necesarry to fix the indices your self even i havent done that in my script … i dont really like this way of coding as there is no documentation (atleast i couldnt find one) about the kbd file so i cannot tell for sure wether the script will work on all the O.S i mean the number (647394) is still a little mysterious and the entire script is based on the number and thats what is really “ugly” about the script, what if it changes in the next version of max … so theres lot of uncertainity about the script, but it works fine on my version of max, may be the better way of coding this would be reading the mainui file and retrieving this number from the file and then using it inside the script that way i think 1 can be assured that it will work on all O.S
however i googled this number and found a couple of kbd files people have posted on the web and all of them have this number, heres 1 :

http://www.resistdesign.com/media/ryan.kbd

#30

Yes it all remains a bit tricky. I think that the least we should do when using the script is making a backup of the users kbd file, just in case something goes horribly wrong.


#31

I was playing around with macro recorder the other day and found some thing interesting about the numbers showing up in the listener for instance :

actionMan.executeAction 0 "50002"  -- Tools: Select and Rotate

the number 50002 is of interest here it can be found in the keyboard file :-

13=3 69 50002 0

which is the shortcut key for rotate “E”
also this number is found in the MaxStartUI file -the file contains information about the menus and the buttons inside them as well as the action numbers associated with each button in the particular menu which tell Max to execute the specific action when the button is pressed, In this case the action number associated with rotate button in the edit menu is 50002 max processes this number by calling the function responsible for showing the rotaion gizmo - :-

Item10_Action=0|50002
CustomTitle_10="Rotate"

next, when i performed actions like going to the edit>Transform Toolbox here’s what the macro recorder shows :

macros.run "PolyTools" "TransformTools"

the correspoing entry in MaxStartUI file reveals this:

Item_13_Mode=2
Item13_Action=647394|TransformTools`PolyTools

this led me to conclude that this number (647394)actually represents an action code which the windows procedure of max processes and decides to call the macros.run command and probably takes as the parameters the values “TransformTools” and "PolyTools "

therfore the line in the kbd file 0=19 69 EPoly_Extrude_Along_Spline`Editable Polygon Object 647394 means pass the macro name “EPoly_Extrude_Along_Spline” in the category "Editable Polygon Object " to the function 647394 which max regards as macros.run function when the keys
19 69 are pressed i.e ALT+E


#32

Yes that sounds reasonable, and I think that these assumptions are all quite correct. :thumbsup:

By the way, I am still working on my version of the code, but here’s a snippet to generate the mod_key + keycode string. It’s loosely based on your code, but I think that had a little error in it: when no mod keys are pressed the mod_key result should be 3.

function createHotKeyString user_hotkey =
(
	user_hotkey = toupper user_hotkey
	local formatted_key = filterstring user_hotkey "+"
	mod_key = 3 --no mod key pressed = bit 1, 2 set.
	key_code = 0
		
	for key in formatted_key do
	(
		case key of
		(
			"SHIFT" : mod_key = bit.or user_mod_k 4;
			"CTRL" : mod_key = bit.or user_mod_k 8;
			"CONTROL" : mod_key = bit.or user_mod_k 8;
			"ALT" : mod_key = bit.or user_mod_k 16;
			default : 
				if (key.count == 1) then 
					key_code = bit.charasint key;
				else
					throw ("Unrecognized key in user_hotkey: " + key);
		)
	)

	--Return string
	(mod_key as string) + " " + (key_code as string)
)

and here’s my take on parsing the kbd file (work in progress)

function assign_hotkey hotkey macro_name macro_category overwrite:false =
(
	local hotkey_string = createHotkeyString hotkey;
	local mainUIPattern = "*=" + hotkey_string + "*0";
	local userPattern = "*=" + hotkey_string + "*647394";
		
	local kbd_filename = actionman.getkeyboardfile();
	local kbd_fileStream = openFile kbd_filename mode:"a+";
		
	local userKeysIndex = 0;
		
	while not eof kbd_fileStream do
	(
		local kbd_line = readline kbd_fileStream;
			
		-- Look for conflicting hotkeys in mainUI and user hotkey groups. 
		-- If found and overwrite == true, function removes the line
		if (matchPattern kbd_line pattern:mainUIPattern OR matchPattern kbd_line pattern:userPattern) then
		(
			print kbd_line;
			if (overwrite == false) then 
				return false;	
			else
				print "not implemented yet"
				--remove line.
		)
		else if (matchPattern kbd_line pattern:"* 647394") do
		(
			userKeysIndex += 1;
		)
	)
	
		
		
	format "%=% %`% 647394
" userKeysIndex hotkey_string macro_name macro_category to:kbd_fileStream;
	close kbd_fileStream;
		
	actionman.loadkeyboardfile kbd_filename;
		
	return true;
)


#33

[left][font=verdana,geneva,lucida,‘lucida grande’,arial,helvetica,sans-serif][/left]
[/font]wow … smart use of the matchpattern function

when no mod keys are pressed the mod_key result should be 3.

thanks for the pointer ! :slight_smile:

ps: .kbd file is case sensitive it regards the ascii 65 as “a” and 97 as “numpad1”… but i think your code will work fine as you have used the toupper() :thumbsup: btw is this for the outliner?


#34

Aha I didn’t know about the numpad ones :slight_smile: Any idea what some of those really large numbers (in the 200s) could be?

btw is this for the outliner?

Yeah, it is initially. Quite a lot of effort for assigning what will probably just be a single hotkey, but oh well, everything for usability I guess…
Of course it should be designed so the code can be used on any project though.


#35

Any idea what some of those really large numbers (in the 200s) could be?

:hmm: not sure … dunno wether its using unicode or ansi maybe the higher range is for keyboards with other layouts … again not sure … normally 65 is ‘A’ and 97 is ‘a’ this is the first time i have seen the ascii of numpad1 is 97… max is full of surprises its like cracking Da vinci’s code :wink:


#36

This is really helpful. Thanks for digging into this, guys.


#37

Hornberger: thanks once again for your input on this issue. Inspired by this I’ve now been able to solve another issue I’ve had for a long time: propagating key events from a .NET control to max.
I’m working on writing a struct to handle both reading and writing the kbd file. Using that, I can now easily run an action assigned to a keyboard shortcut captured in a KeyUp event of a .NET control.

Once I’ve got the adding/writing code in place, I’ll post the code. It is becoming quite a lot though, ~180 lines already…


#38

wow good to know … looking forward for the code release … 180 lines!! … i have done dot net in past for creating simple database apps (infact i still have vb 2008 sitting on my desktop…) but never really integrated it with max… still learning maxscript [sigh] but soon… actually got busy with pflow scripting … its more fun than i thought :stuck_out_tongue:
cheers! :beer:

ps: btw hope the code worked for the outliner. i think you wanted to get the “h” key to pop the outliner instead of the “select by name” window (right?)


#39

Exactly :wink: I noticed how a lot of people who aren’t too familiar with max don’t really know how to assign hotkeys. So doing this automatically (as an option) when installing will take some frustration away.

Cheers :slight_smile:


#40

Ok, here goes :slight_smile: It took a few hours, but I think this is working quite OK.

Using the structs goes as follows:

--create a new KeyboardActionManager instance.
kbd_man = KeyboardActionManager();

--read the keyboard file. default points to the file used by max at the moment.
kbd_man.readactions(); 

--add an action (macro in this case, which will probably be the most common usage)
--if successful, the result will be true. (if replace is omitted or false, the result will be false if no action was added)
kbd_man.addActionFromKeyString "H" kbd_man.macro_table_id macro_name:"outlinerFreezeSelection" macro_category:"Outliner" replace:true;

--write the actions to the max kbd file.
kbd_man.writeActions(); 

--have max reload its kbd file to activate the newly added/replaced shortcut.
kbd_man.maxReloadKeyboardFile();

And the manager code:

/**
 * KeyboardActionManager by P.J. Janssen, www.threesixty.nl
 * Feel free to reuse or modify this code, but please leave the credits in.
 */

/**
 * The KeyboardAction represents a single action assigned to a shortcut key combination.
 *
 * The shortcut is a combination of mod_key_code and key_code. Both of these can be easily obtained from a string or flags
 * through the KeyboardActionManager.getModKeyCode and similar functions.
 *
 * A KeyboardAction should always have a table_id, but depending on the type (Action or Macro), it can have either a persisten_id,
 * or a combination of macro_name and macro_category.
 *
 * The run() function executes the action or macro.
 */
struct KeyboardAction
(
	mod_key_code,
	key_code,
	
	table_id,
	persistent_id,
	
	macro_name,
	macro_category,
	
	
	function isAction =
	(
		(persistent_id != undefined AND table_id != 647394);
	),
	
	function isMacro =
	(
		(macro_name != undefined AND macro_category != undefined AND table_id == 647394);
	),
	
	function run =
	(
		if (isAction()) then
			actionMan.executeAction table_id persistent_id;
		else if (isMacro()) then
			macros.run macro_category macro_name;
	),
	
	
	function compare a1 a2 =
	(
		case of
		(
			(a1.table_id < a2.table_id): -1
			(a1.table_id > a2.table_id): 1
			default: 0
		)
	)
)


/**
 * The KeyboardActionManager struct handles reading and writing kbd files.
 *
 * After being instantiated, the readActions functions should be run, to load the users shortcuts.
 * The actions property is an array containing all loaded actions.
 *
 * This struct was written for some specific needs, and not necessarily to provide a complete interface to kbd files.
 */
struct KeyboardActionManager
(
	actions,
	main_table_id = 0,
	macro_table_id = 647394,
	
	
	/**
	 * GET (MOD)KEYCODE FUNCTIONS
	 */
	 
	-- Returns the mod_key_code based on the modifier key flags provided to the function.
	function getModKeyCode altPressed:false ctrlPressed:false shiftPressed:false =
	(
		local mod_key_code = 3;
		if (shiftPressed) do mod_key_code = bit.or mod_key_code 4;
		if (ctrlPressed) do mod_key_code = bit.or mod_key_code 8;
		if (altPressed) do mod_key_code = bit.or mod_key_code 16;
		
		-- Return mod_key_code.
		mod_key_code;
	),
	
	--Returns the mod_key_code for the supplied string. Format: "ctrl+alt+x"
	function getModKeyCodeFromString key_str =
	(
		key_str = toUpper key_str;
		local str_split = filterString key_str "+";
		local mod_key_code = 3;
		for key in str_split do
		(
			case key of
			(
				"SHIFT" : mod_key_code = bit.or mod_key_code 4;
				"CTRL" : mod_key_code = bit.or mod_key_code 8;
				"CONTROL" : mod_key_code = bit.or mod_key_code 8;
				"ALT" : mod_key_code = bit.or mod_key_code 16;
			)
		)
		
		-- Return mod_key_code.
		mod_key_code;
	),
	
	--Returns the uppercase key_code of the first occurrence of a single character in a string with the format : "ctrl+alt+x"
	function getKeyCodeFromString key_str =
	(
		key_str = toUpper key_str;
		local str_split = filterString key_str "+";
		
		local notfound = true;
		local key_code = 0;
		for key in str_split while notfound do
		(
			if (key.count == 1) do
			(
				key_code = bit.charasint key;
				notfound = false;
			)
		)
		
		--Return the key_code.
		key_code
	),

	
	
	
	
	/**
	 * GET / RUN ACTIONS FUNCTIONS
	 */
	function getActionFromKeyCode mod_key_code key_code table_id1:undefined table_id2:undefined =
	(
		if (actions == undefined) do
			throw "No actions loaded.";
		
		local notfound = true;
		local action;
		for a in actions while notfound do
		(
			if (a.key_code == key_code AND a.mod_key_code == mod_key_code) do
			(
				if (table_id1 == undefined OR a.table_id == table_id1 OR a.table_id == table_id2) do
				(
					action = a;
					notfound = false;
				)
			)
		)
		
		-- Return found action (or undefined if no action was found).
		action;
	),
	
	
	function runActionFromKeyCode mod_key_code key_code table_id1:undefined table_id2:undefined =
	(
		local action = getActionFromKeyCode mod_key_code key_code table_id1:table_id1 table_id2:table_id2;
		if (action != undefined) do
			action.run(); 
	),
	
	
	function runActionFromKeyString str =
	(
		local mod_key_code = getModKeyCodeFromString str;
		local key_code = getKeyCodeFromString str;
		runActionFromKeyCode mod_key_code key_code table_id1:main_table_id table_id2:macro_table_id;
	),
	
	
	
	/**
	 * ADD ACTION TO ACTIONSET
	 */
	function addAction mod_key_code key_code table_id persistent_id:undefined macro_name:undefined macro_category:undefined replace:false =
	(
		if (persistent_id == undefined AND macro_name == undefined) do
			throw "Either persistent_id or macro_name + macro_category parameter required.";
			
		if ((macro_name != undefined AND macro_category == undefined) OR (macro_name == undefined AND macro_category != undefined)) do
			throw "macro_name and macro_category have to be used together.";
			
		if (persistent_id != undefined AND macro_name != undefined AND macro_category != undefined) do
			throw "Using both persistent_id and macro_name + macro_category is not allowed.";

		if (actions == undefined) do
			throw "No actions defined.";
			
		-- Actions that have to be removed are stored in this array to be removed after iteration is completed.
		local removeActions = #();
		
		-- Iterate through actions and check for conflicting actions.
		for a = 1 to actions.count do
		(
			local action = actions[a];
			
			if (action.table_id == table_id) do
			(
				local conflict = false;
				
				-- Check for duplicate key combination.
				if (action.mod_key_code == mod_key_code AND action.key_code == key_code) do conflict = true;
				
				-- Check for duplicate persistent_id if necessary.
				if (persistent_id != undefined) do
					if (action.persistent_id == persistent_id) do 
						conflict = true;

				-- Check for duplicate macro_name and macro_category if necessary.
				if (macro_name != undefined AND macro_category != undefined) do
					if (action.macro_name == macro_name AND action.macro_category == macro_category) do 
						conflict = true;
						
				-- Flag action for removal if it is conflicting with new action and replace is true.
				-- If there are conflicts and replace is false, return false.
				if (conflict AND not replace) then
					return false;
				else if (conflict AND replace) do
					append removeActions a;
			)
		)
		
		-- Remove conflicting actions.
		for a in removeActions do deleteItem actions a;
		
		-- Append new action.
		append actions (KeyboardAction mod_key_code:mod_key_code key_code:key_code table_id:table_id persistent_id:persistent_id macro_name:macro_name macro_category:macro_category);
		
		--Adding was successful, return true;
		true;
	),
	
	
	function addActionFromKeyString key_str table_id persistent_id:undefined macro_name:undefined macro_category:undefined replace:false =
	(
		local mod_key_code = getModKeyCodeFromString key_str;
		local key_code = getKeyCodeFromString key_str;
		addAction mod_key_code key_code table_id persistent_id:persistent_id macro_name:macro_name macro_category:macro_category replace:replace;
	),
	
	
	
	/**
	 * WRITE ACTIONS FILE
	 */
	function writeActions file:(actionMan.getKeyboardFile()) = 
	(
		if (actions == undefined) do
			throw "No actions defined.";
	
		qsort actions KeyboardAction.compare;
		
		--Create a backup of the file we're going to write to.
		local backup_file = file + ".bak";
		if ((getFileSize file) > 0) do
		(
			if ((getFileSize backup_file) > 0) do
				deleteFile backup_file;
				
			if (not (copyFile file backup_file)) do
				throw "Unable to make a backup kbd file. This is too tricky to do without man.";
		)
			
		
		local kbd_fileStream = openFile file mode:"w";
		if (kbd_fileStream == undefined) do
			throw "Unable to write to file." kbdFile;
		
		try (	
			local i = 0;
			local prev_table_id;
			for a in actions do
			(
				if (a.table_id != prev_table_id) do
					i = 0;
					
				if (a.isAction()) then
					format "%=% % % %
" i a.mod_key_code a.key_code a.persistent_id a.table_id to:kbd_fileStream;
				else if (a.isMacro()) then
					format "%=% % %`% %
" i a.mod_key_code a.key_code a.macro_name a.macro_category a.table_id to:kbd_fileStream;
					
				prev_table_id = a.table_id;
				i += 1;
			)
		)
		catch
		(
			-- Restore backup and throw exception.
			close kbd_fileStream;
			deleteFile file;
			copyFile backup_file file;
			--deleteFile backup_file;
			
			throw();
		)
		
		close kbd_fileStream;
	),
	
	
	
	/**
	 * READ & PARSE ACTIONS FILE
	 */
	function readActions kbdFile:(actionMan.getKeyboardFile()) =
	(
		local kbd_fileStream = openFile kbdFile mode:"rS"
		
		if (kbd_fileStream == undefined) do
			throw "Keyboard-File could not be opened." kbdFile;
		
		actions = #();
		
		while (not eof kbd_fileStream) do
		(
			local kbd_line = readLine kbd_fileStream;
			local split_line = filterString kbd_line "= ";
			if (split_line.count &gt; 4) do
			(
				local action = KeyboardAction mod_key_code:(split_line[2] as integer) key_code:(split_line[3] as integer) table_id:(split_line[split_line.count] as integer);
				
				if (split_line.count &gt; 5 OR (matchPattern split_line[4] pattern:"*`*")) then
				(
					--Macro.
					local macro = split_line[4];
					if (split_line.count > 5) do
					(
						for i = 5 to (split_line.count - 1) do 
							macro += " " + split_line[i];
					)
					
					local split_macro = filterString macro "`";
					action.macro_name = split_macro[1];
					action.macro_category = split_macro[2];
				)
				else
				(
					--Action.
					action.persistent_id = split_line[4];
				)
				
				append actions action;
			)
		)
		
		close kbd_fileStream;
	),
	
	
	function maxReloadKeyboardFile =
	(
		actionMan.loadKeyboardFile (actionMan.getKeyboardFile());
	)
)

Now a little disclaimer :slight_smile: The code isn’t heavily tested at all. Use it at your own risk. When writing the kbd file, a backup will be made and restored when something goes wrong, but this is no guarantee that it will never mess something up!
The entries in the kbd file will very likely be shuffled around a little, since the manager sorts all actions by table_id. This shouldn’t be any problem though.

And the runaction part is still a bit rough, especially on the table_id part…


#41

there are three functions to help you add macro to KBD file.


fn makeMacroKBD name category char shift:off ctrl:off alt:off =
(
	local ss = stringstream ""
	local kb = 3
	if shift do kb = bit.set kb 3 on
	if ctrl do kb = bit.set kb 4 on
	if alt do kb = bit.set kb 5 on
	
	format "-1=% % %`% 647394" kb (bit.charasint (toUpper char)) name category to:ss
	ss as string
)
fn isTakenKBD char shift:off ctrl:off alt:off file: macrosOnly:on =
(
	local act
	local ch = (bit.charasint (toUpper char))
	local kb = 3
	if shift do kb = bit.set kb 3 on
	if ctrl do kb = bit.set kb 4 on
	if alt do kb = bit.set kb 5 on
		
	if file == unsupplied do file = actionMan.getKeyboardFile()
	if (ss = openfile file) != undefined do
	(
		skipToString ss "=" 
		while not eof ss and act == undefined do
		(
			str = filterstring (readline ss) " "
			k = execute str[1]
			c = execute str[2]
			i = execute str[str.count]
			if k == kb and c == ch and (not macrosOnly or i == 647394) then act = on else (skipToString ss "=") 
		)
		close ss
		if act == undefined do act = off
	)		
	act
)
fn addMacroKBD name category char shift:off ctrl:off alt:off file: check:on = 
(
	local act = #failed, taken = off
	if file == unsupplied do file = actionMan.getKeyboardFile()
	if (new = not doesfileexist file) or not check or (isTakenKBD char shift:shift ctrl:ctrl alt:alt file:file) != true then
	(
		str = makeMacroKBD name category char shift:shift ctrl:ctrl alt:alt
		ss = if new then createfile file else openFile file mode:"a+"
		if ss != undefined do
		(
			format "%
" str to:ss
			flush ss
			close ss
			act = #added
		)
	)
	else act = #taken
	if act == #added do
	(
		actionMan.loadKeyboardFile file
		actionMan.saveKeyboardFile file
	)
	act
)

enjoy!

PS. use at your own risk.


#42

:applause: wow ! am speechless :wink:


#43

A small note on using the default keyboard file:
It is possible that the file to which actionMan.getKeyboardFile() points, doesn’t exist. With a fresh installation of 3dsmax for example.
Although the script catches this when trying to open it (well, still throws an error, but it’s not terrible), you should keep it in mind. It might be useful to include a function like this in the struct:

function getDefaultKeyboardFile =
(
	local kbd_file = actionMan.getKeyboardFile();
	if (getFileSize kbd_file == 0) do
		actionMan.saveKeyboardFile kbd_file;
		
	kbd_file;
),

#44

1 more for the book of wisdom :thumbsup: i think this is the after effects of the reinstall you had to do to fix the cui.registerdialogbar"???" problem


#45

Yup! :slight_smile: Didn’t fix that issue though…


#46

Thanks for all the hard work on this. But one other thing that would be really useful would be a function to return the shortcut key for a given macroscript. That way we could add a tooltip or additional text to macro button telling the user what the shortcut is.

I’m going to try and parse the code you guys have already posted and see if I can figure out how to do that.


#47

 fn getKeyMacroKBD name category file: =
 (
 	local pattern = "* " + name + "`" + category + " 647394"
 	local hots = #()
 		
 	if file == unsupplied do file = actionMan.getKeyboardFile()
 	if (ss = openfile file) != undefined do
 	(
 		while not eof ss do
 		(
 			str = readline ss
 			sss = filterstring str "= "
 			k = sss[2] as integer
 			c = sss[3] as integer
 			i = sss[sss.count] as integer
 			if matchpattern str pattern:pattern do
 			(
 				ch = toUpper (bit.intaschar c)
 				if bit.get k 5 do ch = "ALT+" + ch 
 				if bit.get k 4 do ch = "CTRL+" + ch 
 				if bit.get k 3 do ch = "SHIFT+" + ch 
 				append hots ch
 			)
 		)
 		close ss
 	)		
 	hots
 )
 

returns all hot_keys as array of strings.

Enjoy!


#48

This thread has been automatically closed as it remained inactive for 12 months. If you wish to continue the discussion, please create a new thread in the appropriate forum.