Intercept a command before it is executed
-
Is there any way for a plugin to intercept a Notepad++ command before it is executed?
I think I already know the answer, and it’s not the one I want to hear. I want to ask before I create an ugly, clunky user experience that will make me ashamed of myself for doing it, just in case I’m missing something.
I really need to trap a command (in this case, Paste) before it is executed, in order to prepare the document so the execution will proceed correctly. It is impractical to keep the document prepared at all times, but in some situations incorrect results follow if the document is not prepared before the paste is executed.
Does anyone know of a way to “catch” a Notepad++ command before Notepad++ executes it?
In case someone wants to know, following is an explanation of why I think there is no other good solution:
My plugin, Columns++, has a long-standing bug with elastic tabstops enabled. (Other implementations of elastic tabstops for Notepad++ have similar failures.)
When pasting a column selection into a document with nothing selected (and some other situations, but that’s the simplest one), the pasted column is inserted at the horizontal position of the cursor, beginning at the line of the cursor and extending down through as many lines as necessary. When elastic tabstops are enabled, that only works if all the lines into which text will be pasted have already had their custom tabstops set — otherwise, the paste will go into random columns based on how the text formats without the custom tabstops set.
The problem is that having Scintilla set custom tabstops on a line is slow. It’s fine for the number of lines that will fit on a screen. It’s painful (as in several seconds) if you have to do it for tens or hundreds of thousands of lines. Even a couple thousand lines can make for annoying sluggishness, depending on what you’re doing that triggers it. So the only practical approach for any elastic tabstops implementation on top of Scintilla is to keep custom tabstops up-to-date on visible lines, and set the rest only when they need to be set.
I can detect when there is a column selection in the clipboard. The problem is that I don’t know any way to detect when the user is about to do a Paste, so I can make sure all lines from the cursor through the depth of the column selection have up-to-date custom tabstops set.
I tried just keeping things up-to-date for the necessary number of lines forward from the cursor whenever there is a column selection in the clipboard. Works OK for, say, a thousand lines.
If you take a fifty-thousand line document and make a column selection from top to bottom, it takes seconds to complete. That’s unfortunate, but acceptable (and it already works that way). Now, if you copy that selection to the clipboard and try keeping things up-to-date for the depth of the selection forward from the cursor (i.e., for the entire document if you move the cursor to the top) every time you move the cursor to a new location, it hangs for seconds. It would have to take time when a paste is actually performed, but it makes no sense to slow practically every action on the document to a crawl just to stay prepared in case the user does a paste. (No doubt I could keep some sort of “dirty” flag to avoid doing this all the time, but it would still come up in situations like typing into a column such that the column gets larger — every keystroke would require setting custom tab stops in the entire document).
The only alternatives to trapping the paste command that have come to my mind suck from a user experience standpoint. I could create a separate “safe paste” command. Users would need to retrain themselves to use it, and/or change the keyboard accelerator for paste and do whatever you have to do to change the toolbar. (Having never had the toolbar enabled, I don’t know what’s involved in customizing it, though I would have to learn if I went this way so as to provide users with sensible instructions.) I could add a ”continuously lay out all tabstops” checkable menu option that one could toggle on so column pastes (and also the built-in Column Editor) would work properly, at the cost of making things impossibly sluggish as files get large when it’s on, and having pastes silently produce incorrect results when it’s off. (Making it imperative to remember to turn it back on, since you wouldn’t get any warning, just a messed-up file.) I could add a “Force full layout now” command that would prepare the document for operations like columns pastes or the column editor, but the user would have to remember to use it.
All just deplorable user experience. I don’t want to do any of those. And I don’t see how else to fix this nasty, long-standing bug… unless I can catch the Paste command.
-
-
@Ekopalypse is right, although I’ve never used
SetWindowSubclass
to do it. I’ve always done it this way–below. Apologies that it is in PythonScript, I know that @Coises is interested in C++…but it shows a method for doing it.I call this script
NppEditor1Editor2HookTestDemo.py
, and note that it only runs under PS3 (although getting it running under PS2 is not too difficult). It intercepts certain things before N++ itself gets hold of them, and prints to the console window what it has intercepted. If you intercept something, you can, at your option, allow N++ to execute it, or prevent that (see comments in code).# -*- coding: utf-8 -*- # references: # https://community.notepad-plus-plus.org/topic/25779/intercept-a-command-before-it-is-executed # for newbie info on PythonScripts, see https://community.notepad-plus-plus.org/topic/23039/faq-desk-how-to-install-and-run-a-script-in-pythonscript from Npp import * import platform from ctypes import (WinDLL, WINFUNCTYPE) from ctypes.wintypes import (LPARAM, HWND, INT, UINT, WPARAM, LPARAM) #------------------------------------------------------------------------------- assert notepad.hwnd assert editor1.hwnd assert editor2.hwnd #------------------------------------------------------------------------------- user32 = WinDLL('user32') LRESULT = LPARAM WndProcType = WINFUNCTYPE( LRESULT, # return type HWND, UINT, WPARAM, LPARAM # arguments ) running_32bit = platform.architecture()[0] == '32bit' SetWindowLong = user32.SetWindowLongW if running_32bit else user32.SetWindowLongPtrW SetWindowLong.restype = WndProcType SetWindowLong.argtypes = [ HWND, INT, WndProcType] GWL_WNDPROC = -4 WM_KEYDOWN = 0x100 WM_SYSKEYDOWN = 0x104 WM_COMMAND = 0x111 #------------------------------------------------------------------------------- class NE1E2HTD(object): def __init__(self): self.npp_new_wnd_proc_for_SWL = WndProcType(self.npp_new_wnd_proc) self.orig_npp_wnd_proc = SetWindowLong(notepad.hwnd, GWL_WNDPROC, self.npp_new_wnd_proc_for_SWL) self.editor1_new_wnd_proc_for_SWL = WndProcType(self.editor1_new_wnd_proc) self.orig_editor1_wnd_proc = SetWindowLong(editor1.hwnd, GWL_WNDPROC, self.editor1_new_wnd_proc_for_SWL) self.editor2_new_wnd_proc_for_SWL = WndProcType(self.editor2_new_wnd_proc) self.orig_editor2_wnd_proc = SetWindowLong(editor2.hwnd, GWL_WNDPROC, self.editor2_new_wnd_proc_for_SWL) def npp_new_wnd_proc(self, hwnd, msg, wParam, lParam): retval = True if msg == WM_COMMAND: print('n++ WM_COMMAND wParam={w}/0x{w:X} lParam={l}/0x{l:X}'.format(w=wParam, l=lParam)) #retval = False # set to False if we don't want further processing of this message by n++ elif msg == WM_KEYDOWN: print('n++ WM_KEYDOWN wParam={w}/0x{w:X} lParam={l}/0x{l:X}'.format(w=wParam, l=lParam)) #retval = False # set to False if we don't want further processing of this message by n++ elif msg == WM_SYSKEYDOWN: print('n++ WM_SYSKEYDOWN wParam={w}/0x{w:X} lParam={l}/0x{l:X}'.format(w=wParam, l=lParam)) #retval = False # set to False if we don't want further processing of this message by n++ if retval: retval = self.orig_npp_wnd_proc(hwnd, msg, wParam, lParam) return retval def common_editor_wnd_proc(self, hwnd, msg, wParam, lParam): retval = True if msg == WM_KEYDOWN: editor_num = 1 if hwnd == editor1.hwnd else 2 print('editor{e} WM_KEYDOWN wParam={w}/0x{w:X} lParam={l}/0x{l:X}'.format(e=editor_num, w=wParam, l=lParam)) #retval = False # set to False if we don't want further processing of this message by n++ return retval def editor1_new_wnd_proc(self, hwnd, msg, wParam, lParam): retval = self.common_editor_wnd_proc(hwnd, msg, wParam, lParam) if retval: retval = self.orig_editor1_wnd_proc(hwnd, msg, wParam, lParam) return retval def editor2_new_wnd_proc(self, hwnd, msg, wParam, lParam): retval = self.common_editor_wnd_proc(hwnd, msg, wParam, lParam) if retval: retval = self.orig_editor2_wnd_proc(hwnd, msg, wParam, lParam) return retval #------------------------------------------------------------------------------- if __name__ == '__main__': try: ne1e2htd print('already running') except NameError: ne1e2htd = NE1E2HTD() print('initialized')
-
-
Because, nowadays, ComCtl32 version is >= 6,
SetWindowSubclass should be the preferred way of subclassing controls.A Rust example of the popup dialog used with the npp-lsp-client.
fn reposition_dialog() { let mut rect = RECT{left: 0, top: 0, right: 0, bottom: 0 }; let lprect = std::ptr::addr_of_mut!(rect); unsafe { if POPUP.is_visible { if let Ok(()) = GetWindowRect(POPUP.statusbar_hwnd, lprect) { POPUP.rect = rect; let width = POPUP.rect.right - POPUP.rect.left; let height = POPUP.rect.bottom - POPUP.rect.top; let _ = SetWindowPos(POPUP.hwnd, HWND_TOP, POPUP.rect.left, POPUP.rect.top, width, height, SWP_NOACTIVATE); let _ = SetWindowPos(POPUP.label_hwnd, POPUP.hwnd, POPUP.rect.left, POPUP.rect.top, width, height, SWP_NOACTIVATE); } } } } extern "system" fn subclassed_statusbar_proc(_hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, _uid_subclass: usize, _dw_ref_data: usize) -> LRESULT { match msg { WM_SIZE | WM_SIZING => { reposition_dialog(); } _ => {} } unsafe { DefSubclassProc(POPUP.statusbar_hwnd, msg, wparam, lparam) } } extern "system" fn subclassed_npp_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, _uid_subclass: usize, _dw_ref_data: usize) -> LRESULT { if msg == WM_MOVING { reposition_dialog(); } unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) } } pub fn create_popup_dialog(hinstance: HMODULE, parent: HWND, fg_color: i32, bg_color: i32) { unsafe { POPUP.hwnd = CreateDialogParamW( hinstance, PCWSTR(IDD_PROGESS as _), parent, Some(dialog_proc), LPARAM(0) ); POPUP.fg_color = fg_color as u32; POPUP.bg_color = bg_color as u32; POPUP.npp_hwnd = parent; let _ = SetWindowSubclass(parent, Some(subclassed_npp_proc), 0, 0); if POPUP.statusbar_hwnd != HWND(0) { let _ = SetWindowSubclass(POPUP.statusbar_hwnd, Some(subclassed_statusbar_proc), 0, 0); } } }
-
Thank you for your replies! I’m glad I asked.
For some reason I hadn’t seriously considered subclassing the main window. Perhaps because it felt “fragile” for a plugin to do that. I did not know about SetWindowSubclass, which, at least on the surface, appears less fragile than the old SetWindowLong method.
I will be trying this approach.
-
@Coises said in Intercept a command before it is executed:
Perhaps because it felt “fragile” for a plugin to do that
… and I guess that’s the case … you never know which plugins will be called before or after … and as I type, I remember that there is also the
SC_MOD_INSERTCHECK
of the SCN_MODIFIED notification, which might be useful in your case too. -
So I’m trying to do the subclass thing in PythonScript (3) and I haven’t yet figured out the proper incantation to get
SetWindowSubclass()
to install my handler as all I get back from it is a zero/false (indicating failure).Perhaps my UINT_PTR and DWORD_PTR type choices are causing the problem; I was unsure how they should be defined for this, as the Windows API seems to want integer values rather than pointers.
Anyway, here’s my code:
# -*- coding: utf-8 -*- from Npp import * from ctypes import (WinDLL, WINFUNCTYPE, c_uint64) from ctypes.wintypes import (BOOL, LPARAM, HWND, UINT, WPARAM) LRESULT = LPARAM assert notepad.hwnd # ensure PS3 comctl32 = WinDLL('comctl32') UINT_PTR = c_uint64 # ??????? DWORD_PTR = c_uint64 # ??????? SubClassProcType = WINFUNCTYPE( LRESULT, # return type HWND, UINT, WPARAM, LPARAM, UINT_PTR, DWORD_PTR # arguments ) SetWindowSubclass = comctl32.SetWindowSubclass SetWindowSubclass.restype = BOOL SetWindowSubclass.argtypes = [ HWND, SubClassProcType, UINT_PTR, DWORD_PTR ] DefSubclassProc = comctl32.DefSubclassProc DefSubclassProc.restype = LRESULT DefSubclassProc.argtypes = [ HWND, UINT, WPARAM, LPARAM ] class SUBCLASS_NPP_WINDOW_DEMO(object): def __init__(self): self.typed_npp_new_wnd_proc_for_subclass = SubClassProcType(self.npp_new_wnd_proc_for_subclass) b = SetWindowSubclass(notepad.hwnd, self.typed_npp_new_wnd_proc_for_subclass, 0, 0) print('SetWindowSubclass() returned:', b) def npp_new_wnd_proc_for_subclass(self, hwnd, msg, wParam, lParam, uIdSubclass, dwRefData): return DefSubclassProc(hwnd, msg, wParam, lParam) SUBCLASS_NPP_WINDOW_DEMO()
Any thoughts or ideas appreciated.
-
afaik you can’t because python scripts are started in a new thread.
From the MS spec
Warning You cannot use the subclassing helper functions to subclass a window across threads.
For python scripts we need to use the old (dangerous ) way.
-
@Ekopalypse said in Intercept a command before it is executed:
For python scripts we need to use the old (dangerous ) way.
Ah…well, I wasted a lot of time then. :-(
I’m sure you didn’t know ahead of time, or you’d have warned me off. :-) -
@Alan-Kilborn said in Intercept a command before it is executed:
I’m sure you didn’t know ahead of time
Read it, forgot about it :-( and then remembered it again when I read your question :-)