Notifications
Clear all

[Closed] Assign action to keyboard shortcut through script

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

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;
)

[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 !

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() btw is this for the outliner?

Aha I didn’t know about the numpad ones 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.

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

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

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…

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 …
cheers!

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?)

1 Reply
(@pjanssen)
Joined: 11 months ago

Posts: 0

Exactly 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

Ok, here goes 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 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…

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.

:applause: wow ! am speechless

Page 4 / 5