Community
    • Login

    Intercept a command before it is executed

    Scheduled Pinned Locked Moved Notepad++ & Plugin Development
    10 Posts 3 Posters 810 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.
    • CoisesC
      Coises
      last edited by Coises

      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.

      EkopalypseE 1 Reply Last reply Reply Quote 3
      • EkopalypseE
        Ekopalypse @Coises
        last edited by

        @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
        • Alan KilbornA
          Alan Kilborn
          last edited by Alan Kilborn

          @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')
                 
          
          EkopalypseE 1 Reply Last reply Reply Quote 4
          • Alan KilbornA Alan Kilborn referenced this topic on
          • EkopalypseE
            Ekopalypse @Alan Kilborn
            last edited by

            @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
            • CoisesC
              Coises
              last edited by

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

              EkopalypseE 1 Reply Last reply Reply Quote 4
              • EkopalypseE
                Ekopalypse @Coises
                last edited by

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

                Alan KilbornA 1 Reply Last reply Reply Quote 1
                • Alan KilbornA
                  Alan Kilborn @Ekopalypse
                  last edited by Alan Kilborn

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

                  EkopalypseE 1 Reply Last reply Reply Quote 0
                  • EkopalypseE
                    Ekopalypse @Alan Kilborn
                    last edited by

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

                    Alan KilbornA 1 Reply Last reply Reply Quote 2
                    • Alan KilbornA
                      Alan Kilborn @Ekopalypse
                      last edited by

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

                      EkopalypseE 1 Reply Last reply Reply Quote 1
                      • EkopalypseE
                        Ekopalypse @Alan Kilborn
                        last edited by

                        @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