Can MXS add custom menus to eg Maxscript editor?


#1

I wonder if you can add scripted custom menus to eg MaxScript editor. Other UIs like the Material / Slate Editor are accessable from Customize User Interface > Menus.

The purpose would be the exact same; to add shortcuts to macroscripts that are related to that particular part of the interface.

Talking of the Maxscript editor.
If you make a selection of something inside the maxscript editor, can maxscript read what you have selected?


#2

Are you sure about Slate ?
Even in 2020 none of its UI are accessible through Customize. (But I’m not sure about the last update 2020.3)
I had to find toolbar handle myself to add a button there
like this:

...
toolbar_hwnd = if toolbar_hwnd == undefined then GetSMEMainToolbarHWND() else toolbar_hwnd
local toolbar = gi.GetICustToolbar (ToIntPtr toolbar_hwnd)

if not ( ButtonExists toolbar_hwnd btnText ) then
(
	local mbdata = gi.macrobuttondata.create macro_settings_ID "" "DropToSlate Settings" 0 "DropToSlate"
	local toolMacroItem = gi.toolmacroitem.create 24 24 mbdata 31
	
	toolbar.AddTool toolMacroItem toolbar.NumItems
...

.

You can easily append menu item using User32.AppendMenu, but then you’ll have to process all menu messages yourself. I had to write c# lib for that purpose.
RFyIolSB77

.

(

	fn GetMXSEditorWindowText hwnd =
	(
		local marshal = dotnetclass "System.Runtime.InteropServices.Marshal"
		str = ""
		try (
			
			len = windows.sendmessage hwnd 0xE 0 0
			lParam = marshal.AllocHGlobal (marshal.SystemDefaultCharSize*(len+1))
			windows.sendmessage hwnd 0xD (len+1) lParam 
				
			ptr = dotnetobject "System.IntPtr" lParam
			str = marshal.PtrToStringAuto ptr
			marshal.FreeHGlobal ptr
			
		) catch ()
		str
	)

	g = (dotNetClass "Autodesk.Max.GlobalInterface").Instance

	hwnd = g.TheMxsEditorInterface.EditorGetEditHWND
	main = g.TheMxsEditorInterface.EditorGetMainHWND

	SCI_GETSELECTIONSTART = 2143
	SCI_GETSELECTIONEND   = 2145

	start = windows.sendMessage hwnd SCI_GETSELECTIONSTART 0 0
	end   = windows.sendMessage hwnd SCI_GETSELECTIONEND   0 0

	doc = GetMXSEditorWindowText hwnd

	substring doc (start+1) (end - start)

)

or use Josef’s MXSEditor


Open an .ms file with Edit()
Open an .ms file with Edit()
#3

nope, I was wrong. Seeing there were menu items for Material Editor made me assume that the same was for Slate.

Unfortunately, my knowledges in MXS are rather low, not to mention c#.
Could you please elaborate this? The c# lib-part, does it mean a plugin or is it still inside the MXS-environment?
Can you provide something that someone like me could adjust for their own need, ie putting menu items that run given macroscripts?

Wow, this is really cool! Love it when it simply works :smiley:
Now that you have sorted this out, I’ll post a new question dedicated for this particular matter.
I’ll check out that link as well.


#4

It’s an extended equivalent of the below code.
upd
btw. you can probably add mxs RCMenu to extend it further


try (::MXSEMenu.destroy())catch()
MXSEMenu = (
	local owner
	struct MXSEMenu
	(
		message_listener =
		(
			fn message_listener =
			(
			local source = ""
			source  = "using System;\n"
			source += "using System.Runtime.InteropServices;\n"
			source += "using System.Windows.Forms;\n"
			source += "class MessageListener : NativeWindow\n"
			source += "{\n"
			source += "public class MsgEventArgs : EventArgs
							{
								public MsgEventArgs( Message message )
								{
									Message = message;
									Handled = false;
								}
								public readonly Message Message;
								public bool Handled = false;
							}
						
							public event EventHandler MessageEvent;											
							
							protected override void WndProc( ref Message message )
							{
								if ( MessageEvent != null )
								{
									MsgEventArgs msg = new MsgEventArgs( message );
									MessageEvent( this, msg );
									
									if ( msg.Handled ) return;
								}

								base.WndProc( ref message );
							}
						}"
				local csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
				local compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
				compilerParams.ReferencedAssemblies.Add("System.dll");
				compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");		
				compilerParams.GenerateInMemory = on
				local compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)		
				compilerResults.CompiledAssembly.CreateInstance "MessageListener"
				dotnetobject "MessageListener"
			)
			message_listener()
		),

		SysMenu =
		(
			fn SysMenu = 
			(
			source = "using System;\n"
			source += "using System.Runtime.InteropServices;\n"
			source += "using System.Windows.Forms;\n"
			source += "class SysMenu\n"
			source += "{\n"
			
			source += "[DllImport(\"user32.dll\", CharSet = CharSet.Auto)]
		static extern bool AppendMenu(IntPtr hMenu, MenuFlags uFlags, uint uIDNewItem, string lpNewItem);

		[DllImport( \"user32.dll\" )]
        public static extern bool RemoveMenu( IntPtr hMenu, uint uPosition, uint uFlags );
		
		[DllImport( \"user32.dll\" )]
        public static extern int GetMenuItemCount( IntPtr hMenu );
				
		[DllImport( \"user32.dll\" )]
		public static extern IntPtr GetSystemMenu( IntPtr hWnd, bool bRevert );

		[DllImport( \"user32.dll\" )]
		public static extern IntPtr GetMenu( IntPtr hWnd );

		[DllImport( \"user32.dll\" )]
		public static extern bool DrawMenuBar( IntPtr hWnd );
			
		[Flags]
		public enum MenuFlags : uint
		{
			MF_STRING = 0,
			MF_BYPOSITION = 0x400,
			MF_SEPARATOR = 0x800,
			MF_REMOVE = 0x1000,
			MF_POPUP = 0x00000010
		}

		public static bool AppendMenu( IntPtr hwnd, string title, int ID )
		{	
			return AppendMenu( hwnd, MenuFlags.MF_BYPOSITION, (uint)ID, title );
		}"
			
			source += "}\n"

			csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
			compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
			compilerParams.ReferencedAssemblies.Add("System.dll");
			compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");

			compilerParams.GenerateInMemory = on
			compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)

			compilerResults.CompiledAssembly.CreateInstance "SysMenu"
				
		)
		SysMenu()
		),

		
		mxse_hwnd = (dotNetClass "Autodesk.Max.GlobalInterface").Instance.TheMxsEditorInterface.EditorGetMainHWND,	
		menu_hwnd = SysMenu.GetMenu (dotNetObject "System.IntPtr" mxse_hwnd) asdotnetobject:true,
		
		menu_ids  = #(),
		callbacks = #(),
		
		-- callback is MAXScriptFunction without arguments
		fn AppendMenu title id callback =
		(
			if findItem menu_ids id > 0 then
			(
				messageBox "ID have already been registered"			
			)
			else
			(
				SysMenu.appendMenu menu_hwnd title id
				SysMenu.DrawMenuBar (dotNetObject "System.IntPtr" mxse_hwnd)
				append menu_ids id
				
				append callbacks ( DataPair id:id execute:callback )
			)
		),
				
		msg_count = 0,		
		
		fn onMessageEvent msg =
		(	
			if msg.message.Msg == 0x0111 do
			(
				owner.msg_count += 1 -- for debug purposes
				
				if findItem owner.menu_ids msg.Message.Wparam > 0 do
				(
					format "Clicked on menu id: % \n" msg.Message.Wparam
					
					local ID = msg.Message.Wparam 
					
					for callback in owner.callbacks where callback.ID == ID do
					(
						try ( callback.execute() ) catch( format "Failed to execute callback: %\n" ID )
					)
				)
			)
		),
		
		fn RemoveMenus =
		(
			local count = SysMenu.GetMenuItemCount menu_hwnd			
			local MF_BYPOSITION = 0x00000400

			for i = count-1 to 9 by -1 do
			(
				SysMenu.RemoveMenu menu_hwnd i MF_BYPOSITION		
			)
			
			SysMenu.DrawMenuBar (dotNetObject "System.IntPtr" mxse_hwnd)			
		),
		
		fn Destroy =
		(		
			message_listener.releaseHandle()
			dotNet.removeAllEventHandlers message_listener		
		),
		
		on create do
		(
			owner = this
			message_listener.assignHandle ( dotNetObject "System.IntPtr" mxse_hwnd )
			dotNet.addEventHandler message_listener "MessageEvent" onMessageEvent		
		)

	)
	
	MXSEMenu()
)



fn On1233 =
(
	messageBox "1233"
)

fn On1234 =
(
	messageBox "1234"
)


RCMenu rcm
(	
	menuitem copyFn "Copy"
	separator s0
	menuitem copyPathFn "Copy Path"
	separator s1
	menuitem gotoFn "Go to source"
	
	on copyFn picked do
	(
		messageBox "Copy"		
	)
	
	on copyPathFn picked do
	(		
		messageBox "Copy Path"		
	)
	
	on gotoFn picked do
	(		
		messageBox "Go to source"	
	)
	
)

fn OnRCM =
(
	PopupMenu rcm
)

MXSEMenu.RemoveMenus()
MXSEMenu.AppendMenu "First" 1233 On1233
MXSEMenu.AppendMenu "Second" 1234 On1234
MXSEMenu.AppendMenu "RCM" 1235 OnRCM

#5

Thank you @Serejah

doc = GetMXSEditorWindowText hwnd print out the whole text from the Macroscript Editor.
Instead of writing substring doc (start+1) (end - start), I wrote substring (GetMXSEditorWindowText hwnd) (start+1) (end - start)

Any comments on that?

About adding menu to the Maxscript Editor. This is still very complex for me. I am trying to understand the part that pinpoints the Maxscript Editor. I hoped to undertand that so that I would try it out on other places, eg Slate or anywhere else that doesn’t allow native menus…

As for the menu making itself.
So menuman.createMenu and menuman.createSubmenuItem isn’t what you use in this case?


#6

Not in this case. In order to append anything to menu you first need to find one and it’s not possible. Otherwise we would see these menus in Customize…

if you don’t want locals printed in listener just wrap whole expression with braces and only the last one will be printed

(
   doc = GetSomeText()
   1 + 1
)

will only print 2 as a result


#7

What about the context menu in the Maxscript Editor? Would you treat it the same way if you wanted to alter it or is yet another thing?


#8

There’s no easy way.
.
What do you want to achieve modifying mxseditor gui ? Quickly execute some custom mxs code?
nNwMZYRiHX



MXSEHotKeyOps =
(
local owner
struct MXSEHotKeyOps (

	public 
		
	dll,
	hwnd,
	hotkeys,
	messagesnooper,
	sendmsg,	
	hotkey_callbacks = #(),
	
	fn msgEvent m =
	(		
		if m.msg == 786 do owner.handleEvent m.wparam  -- 0x0312: ("WM_HOTKEY");		
	),
	
	fn handleEvent index = (
		
		if classof hotkey_callbacks[ index ] == MAXScriptFunction do hotkey_callbacks[ index ]()
		true
	),
	
	fn mxsEditorHWND asdotnet:true = (
	
		for c in (windows.getchildrenhwnd 0) where c[4] == "MXS_SciTEWindow" do for t in (windows.getchildrenhwnd c[1]) where t[4] == "MXS_Scintilla" do (
		
			if asdotnet then return dotNetObject "system.intptr" t[1] else return t[1]
		
		)
		
	),

	fn registerHotKey keycode callbackFn alt:off ctrl:on shift:off = (
			
		hwnd = mxsEditorHWND()
		index = 1 + hotkey_callbacks.count
				
		_alt   = if alt   then 0x0001 else 0x0000
		_ctrl  = if ctrl  then 0x0002 else 0x0000
		_shift = if shift then 0x0004 else 0x0000
		
		modifiers = bit.or (bit.or _alt _ctrl) _shift
		
		if classof callbackFn == MAXScriptFunction do (
			
			regResult = hotkeys.RegisterHotKey hwnd index modifiers keycode
			append hotkey_callbacks callbackFn
			
		)

		if index == 1 do (
			
			messagesnooper.assignHandle hwnd			
			dotnet.addEventHandler messagesnooper "MessageEvent" msgEvent			
		)
		
	),
	
	fn unregisterHotKeys = (
			
		hwnd = mxsEditorHWND()
		messagesnooper.releaseHandle()
		
		for i = hotkey_callbacks.count to 1 by -1 do (
			
			hotkeys.UnRegisterHotKey hwnd i
			
		)
		
		hotkey_callbacks = #()
		ok
	),
	
	fn sendMessage msg wparam str = (
	
		hwnd = mxsEditorHWND asdotnet:false
		sendmsg.sendMessage hwnd msg wparam str
	
	),

	on create do (
	
		local source = ""
		source  = "using System;\n"
		source += "using System.Collections.Generic;\n"
		source += "using System.Text;\n"
		source += "using System.Runtime.InteropServices;\n"
		source += "using System.Windows.Forms;\n"
		source += "\n"
		source += "namespace WinAPI\n"
		source += "{\n"
		source += "class MessageSnooper : NativeWindow\n"
		source += "{\n"
		source += "	public delegate void MessageHandler(object msg);\n"
		source += "	public event MessageHandler MessageEvent;\n"
		source += "	protected override void WndProc(ref Message m)\n"
		source += "	{\n"
		source += "		base.WndProc(ref m);\n"
		source += "		if (MessageEvent != null) MessageEvent((object)m);\n"
		source += "	}\n"
		source += "}\n"
		source += "public class Hotkeys\n"
		source += "{\n"
		source += "        public enum WindowKeys\n"
		source += "        {\n"
		source += "            Alt     = 0x0001,\n"
		source += "            Control = 0x0002,\n"
		source += "            Shift   = 0x0004, // Changes!\n"
		source += "            Window  = 0x0008,\n"
		source += "        }\n"
		source += "        [DllImport(\"user32.dll\")]\n"
		source += "        public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);\n"
		source += "        [DllImport(\"user32.dll\")]\n"
		source += "        public static extern bool UnregisterHotKey(IntPtr hWnd, int id);\n"
		source += "\n"
		source += "        public static void UnregisterHotKeys( IntPtr hwnd )\n"
		source += "        {\n"
		source += "            UnregisterHotKey( hwnd, 1);\n"
		source += "        }\n"
		source += "    }\n"
		source += "public class SendMessageOps\n"
		source += "{\n"
		source += "	[DllImport(\"user32.dll\")]\n"
		source += "	public static extern int SendMessage(Int32 hWnd, int wMsg, int wParam, [MarshalAs(UnmanagedType.LPStr)] string lParam);\n"
		source += "}\n"
		source += "}\n"
				
		csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
		compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
		compilerParams.ReferencedAssemblies.Add("System.dll");
		compilerParams.ReferencedAssemblies.Add("System.IO.dll");
		compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");		
		compilerParams.GenerateInMemory = on
		compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)

		dll = compilerResults.CompiledAssembly.CreateInstance "WinAPI.Hotkeys"	

		owner = this
		hwnd           = mxsEditorHWND()				
		hotkeys        = dotnetobject "WinAPI.Hotkeys"
		messagesnooper = dotnetobject "WinAPI.MessageSnooper"
		sendmsg        = dotnetobject "WinAPI.SendMessageOps"

	)
	
)

try ( ::MXSEHotKeyOps.UnRegisterHotkeys() ) catch()
MXSEHotKeyOps()
)


-- Callback functions without arguments
fn date =
(	
	::MXSEHotKeyOps.sendmsg.sendMessage (::MXSEHotKeyOps.mxsEditorHWND asdotnet:false) 0xC2 1 ("-- Last Modified: " + (dotnetClass "System.DateTime").Now.ToString();)
)

fn ShiftCtrlSpace = messageBox "Callback"

----------------------- Register Ctrl / Alt / Shift + Space combinations -------------------------------------
-- 32 is the key code for Space button
MXSEHotKeyOps.RegisterHotkey 32 date ctrl:on alt:on 
MXSEHotKeyOps.RegisterHotkey 32 ShiftCtrlSpace ctrl:on shift:on
------------------------------------------------------------------------------------------------------------------------------

you can make your own hotkey combination and set it up using this code
key codes


#9

Currently, the ideas I have are (menu or context menu)

  • open script file (as discussed in Open an .ms file with Edit())
  • open folder (based on above, but that opens eg getdir #userScript)
  • Some maxscript-related scripts, eg “Open Script” in Renaming menu items, Create Macroscript-tool (I have a macroscript for that) and so on.
  • your idea, Quick Executing code sounds promising too, so if you have a bunch of code you’d like to share with the community, they could also be part of that.

to summarize: a way to supercharge MXS Editor. Or any other UI that lacks of some buttons/menus they could have.

I know that I can have those tools in the toolbar or a menu in the main UI. But I think offering the user to add menus in where they want can be time saving and UI-friendly :smiley:

Back to the scripts you provided. Could you please provide us some tutorial? Honestly, it’s rather complex code, since its a complex task.
Or a “front end” / customization part at the beginning of the script that lets you type in the relevant UX stuff, , followed by a -- Do not alter below this line where the “backend” part is. We, the non-skilled users, can simply ignore that part.


#10

I would rather suggest you to use VSCode and completely forget about the editor build in max if you really need extended editor functionality. You’ve chosen a tough task to begin with.
.
As of tutorial just scroll down to the bottom of the source code. Two hotkey examples are provided. There’s nothing complicated even for a beginner level scripter.
But don’t expect too much of the code that I posted above. It needs testing, debugging etc…
.

I doubt it will ever happen to slate or mxseditor.


#11

+1 to that. On top of being a much better editor it has the benefit that, being a separate application, if Max crashes you don’t lose your unsaved script changes. (not that it ever happens to me :smiley: )