Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size
-
@fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:
That’s interesting! I’d think that a NP++ plugin can only do things that are also possible in NP++ itself since the underlying engine is the same.
There is a way to do it in Scintilla:
SCI_SETMARGINLEFT and SCI_SETMARGINRIGHT set margins, in pixels.A plugin, or a PythonScript, could determine the displayable text width of the Scintilla control (I think that would require getting the client width of the Scintilla control with GetClientRect, then using SCI_GETMARGINS to find the number of active margins and iterating through them with SCI_GETMARGINWIDTHN to subtract the margins; there might be an easier way that hasn’t occurred to me) and the width of a string of characters of the desired length (SCI_TEXTWIDTH with style 0), subtract, divide by two and set that margin on each side.
-
@Coises said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:
There is a way to do it in Scintilla:
SCI_SETMARGINLEFT and SCI_SETMARGINRIGHT set margins, in pixels.Not that I’d use it much, but there were a couple of times I wished the feature was there. Now you have pointed out what the underlying engine can do for this (it works, I’ve checked!).
For the real life, it’s of no use though. Who would want to set the width in pixels? To be usable by an end user, it needs a script that would calculate pixel width for the current font and also handle widows resize.
These are just theoretical thoughts since you also advised a practicl way to set the desired width (with side panels).
Thank you!
-
@fml2 Yes, currently gq in visual line mode works with custom width. I haven’t thought about that and didn’t update wrap command.
-
@fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:
To be usable by an end user, it needs a script that would calculate pixel width for the current font and also handle widows resize.
Which is why @Coises suggested that the plugin or PythonScript do the calculation, including getting the active window (or really, editor sub-window) size, the margins, and calculating character width based on the font size.
The one thing he didn’t suggest is hooking this function to a specific notification (for “handle windows resize”), but that was rather implied by the solution. In case you’re not sure, I believe that handling SCN_UPDATEUI is the right notification, because there isn’t one specific to resize. In PythonScript, that hook would be set using
editor.callback(functionNameHere, [SCINTILLANOTIFICATION.UPDATEUI])– sofunctionNameHere()would be the function that does the calculations and then sets the marginleft/marginright. (personally, I’d recommend just changing the margin-right based on the full width, rather than dividing it by 2 and splitting between left-and-right) -
My rough implementation of @Coises suggestion is as follows
# 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 # 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 pysc_setWrap80e1(args=None): pysc_setWrap80(editor1) def pysc_setWrap80e2(args=None): pysc_setWrap80(editor2) def pysc_setWrap80eX(args=None): pysc_setWrap80(editor) editor.callback(pysc_setWrap80eX, [SCINTILLANOTIFICATION.PAINTED]) console.write("SetWrap80 registered callback\n")(this script tested in PythonScript 3)
The FAQ (https://community.notepad-plus-plus.org/topic/23039/faq-how-to-install-and-run-a-script-in-pythonscript) explains how to run a script, or how to make it run automatically at startup
The script registers the PAINTED notification – I found that UPDATEUI doesn’t happen everytime you change the window width, whereas PAINTED does… but it means that the callback is running a lot. I wish I knew of a notification that was better suited to just-on-resize, but I don’t. Maybe one of the other PythonScript experts can chime in with a better notification to use (or other suggestions for improvements). I just figured I’d do a proof of concept.
If you aren’t changing window size all that often, then it would be better to make a copy of that script that just re-adjusts the 80-character margin on demand (ie, when you run the script) by not using the
editor.callback(...)line, and instead just callingpysc_setWrap80(editor)at the end. -
P PeterJones referenced this topic on
-
@PeterJones said:
I wish I knew of a notification that was better suited to just-on-resize
Hook the message loop and look for WM_SIZE messages?
-
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 retvalfile 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\scriptsdirectory (or equivalent for non-AppData setups, or you need to have added their parent directory tosys.path) -
P PeterJones referenced this topic on
-
Thank you all for the great proposals and for showing what is possible in NP++. However, I find the solution (to the problem I rarely have) a bit too complicated. In the cases I need wrapping at a specific position I’d hence rather use the proposal of @Coises with a side panel. Or resize the window.
-
Hello, @thorsten-heuer, @h-jangra, @fml2, @coises, @m-andre-z-eckenrode, @peterjones, @alan-kilborn and All,
Here is other way to get something CLOSE to a soft wrap at column
80, using theDstraction Free Modefeature of Notepad++This method do a soft wrap at column
81( cannot do better ! )Of course, I suppose that it depends of the width of the current screen (Personally, my laptop screen is
34,5cm wide). Thus, some adjustments will certainly be required !However, the nice thing is that the
Distraction Free Modeuse the totality of your current screen, even if you run N++, in a narrowed window !
- In a new tab, paste this text :
1234567890 123456789012345678901234567890123456789012345678901234567890123456789012345678901 123456789012345678901234567890123456789012345678901234567890123456789012345678901 123456789012345678901234567890123456789012345678901234567890123456789012345678901 123456789012345678901234567890123456789012345678901234567890123456789012345678901 123456789012345678901234567890123456789012345678901234567890123456789012345678901 1234567890As you can see, it contains
5lines of81characters-
Select
Settings > Shortcut Mapper... > Main -
Move to line
196 -
Attibute the
F9shortcut to the Distraction Free Mode (or any other if already used !) -
Valid and exit the
Shortcut Mapper -
Select the
View > Word wrapoption, if necessary -
In the Toolbar, un-select the
Show All Charactersicon (¶), if necessary -
Select The default zoom (
Ctrl + /) -
Then, increase the zoom value by
3times (Ctrl + +3 times ) -
Select
Settings > Preferences... > Margins/Border/Edge > Padding -
Select the
0value for the left and right options -
Select the
5value for the Distraction Free option -
Hit the Close button
-
Select
Settings > Style Configurator...> Font Style -
Choose one of these monospaced fonts
Aptos - Courier New - DejaVu Sans Mono - Lucida Console - Lucida Sans Typewriter -
Click on the
Save & Closebutton
Now :
-
Switch to the new tab, mentionned above
-
Hit the
F9key to get theFree Distraction Mode -
Move at the end of a line containing
81characters -
Add the digit
2=> the current line is split and the2digit is reported at beginning of next line !
Again, it works with my configuration but just adapt to your needs !
Best Regards
guy038
Remainder :
Don’t use the
Alt + Tabshortcut to switch to an other application, when using the N++Distraction Free Modeas hitting again on theF9key will not return to normal N++ view !Simply repeat the
Alt + Taboperation to return to N++ first and then hit theF9shortcut -
@Thorsten-Heuer , @fml2 , @PeterJones :
I made a simple plugin to do this, which you can find here:
https://github.com/Coises/Marginalize/
There is a limitation to this method of wrapping. This is true whether you drag the window edge, open a panel or use a script or a plugin:
In most contexts, such as in a browser, soft wrapping replaces a space with a new line when breaking between words. When Scintilla wraps plain text at a space, the space is still included on the line before the wrap. So a word that should fit exactly to the margin will be pushed to the next line instead, because there isn’t room for the space.
When a language other than “None (Normal Text)” is in effect, spaces can be pushed to the beginning of a line. (I think what happens is that lines can wrap at any point where the style changes, but I haven’t verified that.)
Either way, you will not get exactly what soft wrapping normally is, because Scintilla always displays spaces somewhere when wrapping on a space.
Like @PeterJones, I was surprised to find that SCN_UPDATEUI isn’t issued when the window is resized… but not too surprised, since I had already come across another situation, zooming, where you would expect it to trigger, but it doesn’t.
So at first I tried trapping SCN_UPDATEUI and SCN_ZOOM and also subclassing the two Scintilla controls to watch for WM_SIZE messages. I wondered, though, what else might not be included in SCN_UPDATEUI that I didn’t know. I decided that, at least working in C++, there was no obvious advantage to avoiding SCN_PAINTED, which I think is bound to catch everything that matters, without getting into subclassing. Note that SCN_UPDATEUI itself is called quite often, too — I’m not really sure how much worse SCN_PAINTED can be than SCN_UPDATEUI plus a hook or a subclass.
It appears that setting the margins can at least sometimes cause a repaint even when the new margins are the same as the old. In any case, I found it necessary to be especially careful that a window too narrow to allow for the specified width didn’t create some very strange artifacts by repeatedly setting the margins to zero when they already were zero.
-
@Coises Thank you for the quick implementation! If enebaled, it applies to all documets, not just the current one, right?
-
Hello, @coises and All,
I’ve just tried you last
Marginalizeplugin and the results are awesome !It’s particularly the case when using the
Center the workspaceoption, inPlugins > Marginalize > Settings, which mimics theDistraction Free Mode, while retaining the standard screen !I’m sure that this plugin will become a MUST very soon for any N++ user !
Many thanks, again, for this valuable plugin !
Best Regards,
guy038
-
@fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:
@Coises Thank you for the quick implementation! If enebaled, it applies to all documets, not just the current one, right?
Yes, I made it a single toggle for all documents in both views.
It would be possible, but considerably more complex, to track documents and enable/disable per document. (I do it with elastic tabstops in Columns++.)