• Login
Community
  • Login

Intercept a command before it is executed

Scheduled Pinned Locked Moved Notepad++ & Plugin Development
10 Posts 3 Posters 793 Views
Loading More Posts
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • C
    Coises
    last edited by Coises May 14, 2024, 2:40 AM May 14, 2024, 2:38 AM

    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.

    E 1 Reply Last reply May 14, 2024, 7:44 AM Reply Quote 3
    • E
      Ekopalypse @Coises
      last edited by May 14, 2024, 7:44 AM

      @Coises

      The only way I know of is to subclass Npps, and maybe Scintillas, WndProc in order to get the messages before them.

      1 Reply Last reply Reply Quote 2
      • A
        Alan Kilborn
        last edited by Alan Kilborn May 14, 2024, 12:10 PM May 14, 2024, 11:43 AM

        @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')
               
        
        E 1 Reply Last reply May 14, 2024, 12:32 PM Reply Quote 4
        • A Alan Kilborn referenced this topic on May 14, 2024, 11:43 AM
        • E
          Ekopalypse @Alan Kilborn
          last edited by May 14, 2024, 12:32 PM

          @Alan-Kilborn

          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);
                  }
              }
          }
          
          1 Reply Last reply Reply Quote 1
          • C
            Coises
            last edited by May 14, 2024, 4:21 PM

            @Ekopalypse, @Alan-Kilborn:

            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.

            E 1 Reply Last reply May 15, 2024, 6:08 AM Reply Quote 4
            • E
              Ekopalypse @Coises
              last edited by May 15, 2024, 6:08 AM

              @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.

              A 1 Reply Last reply Jun 5, 2024, 2:09 PM Reply Quote 1
              • A
                Alan Kilborn @Ekopalypse
                last edited by Alan Kilborn Jun 5, 2024, 2:14 PM Jun 5, 2024, 2:09 PM

                @Ekopalypse

                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.

                E 1 Reply Last reply Jun 5, 2024, 7:55 PM Reply Quote 0
                • E
                  Ekopalypse @Alan Kilborn
                  last edited by Jun 5, 2024, 7:55 PM

                  @Alan-Kilborn

                  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.

                  A 1 Reply Last reply Jun 5, 2024, 9:21 PM Reply Quote 2
                  • A
                    Alan Kilborn @Ekopalypse
                    last edited by Jun 5, 2024, 9:21 PM

                    @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. :-)

                    E 1 Reply Last reply Jun 6, 2024, 5:17 AM Reply Quote 1
                    • E
                      Ekopalypse @Alan Kilborn
                      last edited by Jun 6, 2024, 5:17 AM

                      @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 :-)

                      1 Reply Last reply Reply Quote 1
                      • First post
                        Last post
                      The Community of users of the Notepad++ text editor.
                      Powered by NodeBB | Contributors