@Alan-Kilborn ,
Okay, since I didn’t know how to hook the message loop I found this old post by you (I remembered you had done it for the alt+scrollwheel, thankfully), and I was able to use that as a basis for a two-file solution to @fml2’s request
file 1: MsgHooker.py
# -*- coding: utf-8 -*-
# original author: @Alan-Kilborn
# reference: https://community.notepad-plus-plus.org/post/100127
# modified by: @PeterJones
# - updated to add the .unhook() and .__del__() methods
import platform
from ctypes import (WinDLL, WINFUNCTYPE)
from ctypes.wintypes import (HWND, INT, LPARAM, UINT, WPARAM)
user32 = WinDLL('user32')
GWL_WNDPROC = -4 # used to set a new address for the window procedure
LRESULT = LPARAM
WndProcType = WINFUNCTYPE(
LRESULT, # return type
HWND, UINT, WPARAM, LPARAM # function arguments
)
running_32bit = platform.architecture()[0] == '32bit'
SetWindowLong = user32.SetWindowLongW if running_32bit else user32.SetWindowLongPtrW
SetWindowLong.restype = WndProcType
SetWindowLong.argtypes = [ HWND, INT, WndProcType ]
class MH(object):
def __init__(self,
hwnd_to_hook_list,
hook_function, # supplied hook_function must have args: hwnd, msg, wparam, lparam
# and must return True/False (False means the function handled the msg)
msgs_to_hook_list=None, # None means ALL msgs
):
self.users_hook_fn = hook_function
self.msg_list = msgs_to_hook_list if msgs_to_hook_list is not None else []
self.new_wnd_proc_hook_for_SetWindowLong = WndProcType(self._new_wnd_proc_hook) # the result of this call must be a self.xxx variable!
self.orig_wnd_proc_by_hwnd_dict = {}
for h in hwnd_to_hook_list:
self.orig_wnd_proc_by_hwnd_dict[h] = SetWindowLong(h, GWL_WNDPROC, self.new_wnd_proc_hook_for_SetWindowLong)
v = self.orig_wnd_proc_by_hwnd_dict[h]
#print(f"add {h:08x} => {v}")
def __del__(self):
self.unhook()
def unhook(self):
#print(f"unhook: self:{self} => <{self.orig_wnd_proc_by_hwnd_dict}>");
mykeys = []
for h in self.orig_wnd_proc_by_hwnd_dict.keys():
orig = self.orig_wnd_proc_by_hwnd_dict[h]
print(f"\tdel {h:08x} => {orig}")
SetWindowLong(h, GWL_WNDPROC, orig)
mykeys.append(h)
for h in mykeys:
del self.orig_wnd_proc_by_hwnd_dict[h]
def _new_wnd_proc_hook(self, hwnd, msg, wParam, lParam):
retval = True # assume that this message will go unhandled (by us)
need_to_call_orig_proc = True
if len(self.msg_list) == 0 or msg in self.msg_list:
retval = self.users_hook_fn(hwnd, msg, wParam, lParam)
if not retval: need_to_call_orig_proc = False
if need_to_call_orig_proc:
retval = self.orig_wnd_proc_by_hwnd_dict[hwnd](hwnd, msg, wParam, lParam)
return retval
file 2: the main script:
# encoding=utf-8
"""in response to https://community.notepad-plus-plus.org/topic/27351/
Trying to implement @Coises idea for setting the wrap to exactly 80
"""
from Npp import *
import ctypes
from ctypes import wintypes
from MsgHooker import MH as MsgHook
WM_SIZE = 0x0005
# Define the RECT structure to match Win32 API
class RECT(ctypes.Structure):
_fields_ = [
("left", wintypes.LONG),
("top", wintypes.LONG),
("right", wintypes.LONG),
("bottom", wintypes.LONG)
]
def width(self):
return self.right - self.left
def height(self):
return self.bottom - self.top
def pysc_setWrap80(ed=editor):
#console.write("ed={}\n".format(ed))
WRAPCHARS = 80
# Setup the Win32 function prototype
user32 = ctypes.windll.user32
user32.GetClientRect.argtypes = [wintypes.HWND, ctypes.POINTER(RECT)]
user32.GetClientRect.restype = wintypes.BOOL
def get_window_size(hwnd):
# 2. Instantiate the RECT structure
rect = RECT()
# 3. Call GetClientRect passing the rect by reference
if user32.GetClientRect(hwnd, ctypes.byref(rect)):
# 4. Parse the results
# Client coordinates: top-left is always (0,0)
return rect
else:
raise Exception("GetClientRect failed")
sz = get_window_size(ed.hwnd)
#console.write("{} => {}\n".format(ed.hwnd, {"width": sz.width(), "height": sz.height()}))
usableWidth = sz.width()
for m in range(0, 1+ed.getMargins()):
w = ed.getMarginWidthN(m)
usableWidth -= w
#console.write("m#{}: {} => usableWidth: {}\n".format(m, w, usableWidth))
widthWrappedChars = ed.textWidth(0,"_"*WRAPCHARS)+1 # one extra pixel to be able to show the VerticalEdge indicator line
wantMargin = usableWidth - widthWrappedChars
if wantMargin < 1:
wantMargin = 0
#console.write("{}\n".format({"windowWidth": sz.width(), "usableWidth": usableWidth, "pixelsFor80Char": widthWrappedChars, "wantMargin": wantMargin}))
ed.setMarginRight(wantMargin)
ed.setMarginLeft(0)
def HIWORD(value): return (value >> 16) & 0xFFFF
def LOWORD(value): return value & 0xFFFF
def pysc_size_callback( hwnd, msg, wParam, lParam):
#console.write(f"cb(h:{hwnd}, m:{msg}, w:{wParam}, l:{lParam}) => {LOWORD(lParam)} x {HIWORD(lParam)}\n")
if hwnd == editor1.hwnd:
pysc_setWrap80(editor1)
elif hwnd == editor2.hwnd:
pysc_setWrap80(editor2)
return True
pysc_setWrap80(editor1)
pysc_setWrap80(editor2)
pysc_size_hook = MsgHook([ editor1.hwnd, editor2.hwnd ], pysc_size_callback, [WM_SIZE])
console.write("SetWrap80 registered WM_SIZE callback\n")
def pysc_unsetWrap80(args=None):
"""
To stop
pysc_unsetWrap80()
"""
editor1.setMarginRight(0)
editor2.setMarginRight(0)
global pysc_size_hook
if pysc_size_hook:
pysc_size_hook.unhook()
del pysc_size_hook
# use the following in the console (no #) to stop it from always wrapping at 80
#
# pysc_unsetWrap80()
You run the main script (or call it from startup.py) to get it to start watching for WM_SIZE, and when it gets that, it does the calcs needed to keep the wrap margin at 80 characters (+1 pixel so that the Vertical Edge line would be visible if you’ve got it on)
Both scripts should go in the main %AppData%\Notepad++\plugins\Config\PythonScript\scripts directory (or equivalent for non-AppData setups, or you need to have added their parent directory to sys.path)