Notepad++ official documentation is online now
-
Please add a chapter about context menu customization.
-
@dinkumoil said:
Please add a chapter about context menu customization.
The old documentation for this is (sort of) here. Seems like it is not bad; could be used as a basis for something new.
Reading it now, I see:
The Tab Bar Context Menu
For completeness’ sake, there is another menu that would qualify as a context menu. It is the one that pops up when right clicking a tab in any view. However, there isn’t much to say about it here, as it is not configurable. This is not related with its being practical. Plugins can manipulate it.The part I bolded got me thinking “I wonder if @Ekopalypse has some nice Pythonscript code for showing how to do this, for adding/removing items?”. Eko was certainly a master Pythonscripter for getting the Shortcut Mapper to cough up its mappings, so… :)
-
@Ekopalypse Does the upvote mean you’re going to share something soon? :)
-
for adding/removing items
Do you mean adding/removing on the fly aka at runtime?
If so, never thought about it.
I assume hooking the window procedure is the way to go. Hmm, interesting.Eko was certainly a master Pythonscripter
Lately I learned that my knowledge about python isn’t that good :-(
-
@Ekopalypse said in Notepad++ official documentation is online now:
Do you mean adding/removing on the fly aka at runtime?
I suppose I mean somehow achieving with P.S. a way to customize this (tab bar rclick context) menu, much like the regular (editor window rclick) context menu can be customized. The method would be very different, but the results would be similar.
Lately I learned that my knowledge about python isn’t that good
I’m having a hard time believing that.
-
for testing my notepad wrapper methods I’ve modified the test from the pythonscript plugin
from threading import Thread import queue item_count = -1 def start_monitor(_q): MN_GETHMENU = 0x1E1 i = 10 tabbar_context_menu_hwnd = 0 while i: tabbar_context_menu_hwnd = user32.FindWindowExW(None, None, '#32768', None) log(tabbar_context_menu_hwnd) if tabbar_context_menu_hwnd: break else: time.sleep(.1) i -= 1 if tabbar_context_menu_hwnd: menu_handle = user32.SendMessageW(tabbar_context_menu_hwnd, MN_GETHMENU, 0, 0) global item_count item_count = user32.GetMenuItemCount(menu_handle) _q.put(item_count) user32.SendMessageW(tabbar_context_menu_hwnd, WM_CLOSE, 0, 0) q = queue.Queue() t = Thread(target=start_monitor, args=(q,)) t.start() self.assertIsNone(notepad.triggerTabbarContextMenu(0,0)) t.join() item_count = q.get() self.assertNotEqual(item_count, -1, msg="Can't find popup menu") self.assertEqual(item_count, 28, msg=f'Expected 28 menu items but received:{item_count}')
so yes, it is possible to use it as an starting point, I guess.
Let me think about it.I’m having a hard time believing that.
Then you better don’t check the official python mailing list :-(
-
Just a word of warning - don’t run the current code unmodified as it might
hang npp. -
basically you run this once to register your menu items
and the hook. Then every time you right click the tabbar menu you
should see your newly created menu items.
Not sure whether this is the most optimized version but I guess
a good starting point.The critical parts is the notepad.allocateCmdID and passing this value
to the menuitem wID field.MENU_MAPPER holds this id and the function which should get executed.
from threading import Thread import queue import time import ctypes from ctypes import wintypes import platform user32 = ctypes.WinDLL('user32', use_last_error=True) MENU_MAPPER = {} LRESULT = wintypes.LPARAM ULONG_PTR = wintypes.LPVOID WndProcType = ctypes.WINFUNCTYPE(LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM) CallWindowProc = user32.CallWindowProcW CallWindowProc.restype = LRESULT CallWindowProc.argtypes = [WndProcType, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] x86 = platform.architecture()[0] == '32bit' SetWindowLong = user32.SetWindowLongW if x86 else user32.SetWindowLongPtrW SetWindowLong.restype = WndProcType SetWindowLong.argtypes = [wintypes.HWND, wintypes.INT, WndProcType] CONFIGURATION = None MFS_ENABLED = 0x0 MFS_DISABLED = 0x3 MIIM_STATE = 1 MIIM_ID = 2 MIIM_SUBMENU = 4 MIIM_STRING = 64 MFT_STRING = 0 MFS_ENABLED = 0 class MENUITEMINFO(ctypes.Structure): ''' Implements the winapi MENUITEMINFO structure''' _fields_ = [('cbSize', wintypes.UINT), ('fMask', wintypes.UINT), ('fType', wintypes.UINT), ('fState', wintypes.UINT), ('wID', wintypes.UINT), ('hSubMenu', wintypes.HMENU), ('hbmpChecked', wintypes.HBITMAP), ('hbmpUnchecked', wintypes.HBITMAP), ('dwItemData', ULONG_PTR), ('dwTypeData', wintypes.LPWSTR), ('cch', wintypes.UINT), ('hbmpItem', wintypes.HBITMAP),] class Hook(): def __init__(self): self.nppHandle = user32.FindWindowW(u'Notepad++', None) def register(self): self.newWndProc = WndProcType(self.MyWndProc) self.prevWndProc = SetWindowLong(self.nppHandle, -4, self.newWndProc) def MyWndProc(self, hWnd, msg, wParam, lParam): if msg == 273 and wParam in MENU_MAPPER: MENU_MAPPER[wParam]() # hand control back to npp return CallWindowProc(self.prevWndProc, hWnd, msg, wParam, lParam) def modify_context_menu(): i = 10 tabbar_context_menu_hwnd = 0 while i: tabbar_context_menu_hwnd = user32.FindWindowExW(None, None, u'#32768', None) if tabbar_context_menu_hwnd: menu_handle = user32.SendMessageW(tabbar_context_menu_hwnd, MN_GETHMENU, 0, 0) mii = MENUITEMINFO() mii.cbSize = ctypes.sizeof(mii) mii.fMask = MIIM_ID | MIIM_STRING mii.fType = MFT_STRING mii.fState = MFS_ENABLED startid = notepad.allocateCmdID(2) mii.wID = startid mii.dwTypeData = "TEST1" user32.InsertMenuItemW(menu_handle, 0, True, ctypes.byref(mii)) MENU_MAPPER[startid] = lambda: console.write('TEST1 clicked\n') mii.wID = startid+1 mii.dwTypeData = "TEST2" user32.InsertMenuItemW(menu_handle, 0, True, ctypes.byref(mii)) MENU_MAPPER[startid+1] = lambda: console.write('TEST2 clicked\n') break else: time.sleep(.1) i -= 1 MN_GETHMENU = 0x1E1 t = Thread(target=modify_context_menu) t.start() notepad.triggerTabbarContextMenu(0,6) t.join() hook = Hook() hook.register() print('done')
-
So, it does work to achieve the goal, but 2 immediate things were noticed.
-
import queue
fails because there is no modulequeue
in the Python that ships with Pythonscript. However, this isn’t a real problem, because the code doesn’t even usequeue
. Easy enough to remove the line. Done. -
A bigger problem; when exiting Notepad++ after this “new feature” has been added, I get this, every time:
-
-
Do you already have another HOOK registered?
Which Npp version and which PS version are you currently running?Right, in python3 it is called queue and in python2 it is called Queue.
Removed the code but forgot to remove the import.This part is critical if it is already used but maybe slightly different defined.
LRESULT = wintypes.LPARAM ULONG_PTR = wintypes.LPVOID WndProcType = ctypes.WINFUNCTYPE(LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM) CallWindowProc = user32.CallWindowProcW CallWindowProc.restype = LRESULT CallWindowProc.argtypes = [WndProcType, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] x86 = platform.architecture()[0] == '32bit' SetWindowLong = user32.SetWindowLongW if x86 else user32.SetWindowLongPtrW SetWindowLong.restype = WndProcType SetWindowLong.argtypes = [wintypes.HWND, wintypes.INT, WndProcType]
-
@Ekopalypse said in Notepad++ official documentation is online now:
Do you already have another HOOK registered?
Very likely. Is this a problem?
Which Npp version and which PS version are you currently running?
7.7.1 32-bit PS1.3.0.0
PS1.4.0.0 has a trivial amount of difference from 1.3.0.0 so that’s why.
This part is critical if it is already used but maybe slightly different defined.
This I don’t understand.
Are you glad I got you involved in this? :)
-
Are you glad I got you involved in this? :)
Yes of course :-D, and actually I do something similar with python3script while handling the
menu items (in PS when you create a script it appears in the script menu - I do the same) so it is important for me as well.If you hook the message queue you need to ensure that data passed and provided do not get mangled in between
because at then end npp needs to handle the messages as well.I actually never tested whether this codes work with 32bit and because of the following
LRESULT = wintypes.LPARAM CallWindowProc.restype = LRESULT x86 = platform.architecture()[0] == '32bit' SetWindowLong = user32.SetWindowLongW if x86 else user32.SetWindowLongPtrW
it might be that I’m doing the wrong thing.
Let me check with a npp 32bit version and msdn. -
seems my code is correct and works on npp and PS 32bit version.
According to MSDN SetWindowLongPtrW is for x64 and SetWindowLongW for 32bit.
WindowProc callback function returns LRESULT which is defined astypedef LONG_PTR LRESULT;
the same as LPARAM (wich is also typedef LONG_PTR LPARAM;)
LONG_PTR is defined to be int64 on 64bit and long on 32bit#if defined(_WIN64) typedef __int64 LONG_PTR; #else typedef long LONG_PTR; #endif
So I don’t see a problem with my code what means that one of your other hook(s),
defines something slightly different and corrupts the message handling, I guess. -
@Ekopalypse said in Notepad++ official documentation is online now:
one of your other hook(s),
defines something slightly different and corrupts the message handlingOkay, I will go examine those others and use your way of doing it as the definitive way it should be done.