Community
    • Login

    Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size

    Scheduled Pinned Locked Moved Help wanted · · · – – – · · ·
    22 Posts 8 Posters 848 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.
    • PeterJonesP
      PeterJones @fml2
      last edited by PeterJones

      @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]) – so functionNameHere() 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)

      PeterJonesP 1 Reply Last reply Reply Quote 2
      • PeterJonesP
        PeterJones @PeterJones
        last edited by

        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 calling pysc_setWrap80(editor) at the end.

        Alan KilbornA 1 Reply Last reply Reply Quote 0
        • PeterJonesP PeterJones referenced this topic on
        • Alan KilbornA
          Alan Kilborn @PeterJones
          last edited by

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

          PeterJonesP 1 Reply Last reply Reply Quote 0
          • PeterJonesP
            PeterJones @Alan Kilborn
            last edited by

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

            1 Reply Last reply Reply Quote 2
            • PeterJonesP PeterJones referenced this topic on
            • fml2F
              fml2
              last edited by

              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.

              1 Reply Last reply Reply Quote 0
              • guy038G
                guy038
                last edited by guy038

                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 the Dstraction Free Mode feature 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,5 cm wide). Thus, some adjustments will certainly be required !

                However, the nice thing is that the Distraction Free Mode use 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
                1234567890
                
                
                

                As you can see, it contains 5 lines of 81 characters

                • Select Settings > Shortcut Mapper... > Main

                • Move to line 196

                • Attibute the F9 shortcut to the Distraction Free Mode (or any other if already used !)

                • Valid and exit the Shortcut Mapper

                • Select the View > Word wrap option, if necessary

                • In the Toolbar, un-select the Show All Characters icon ( ¶ ), if necessary

                • Select The default zoom ( Ctrl + / )

                • Then, increase the zoom value by 3 times ( Ctrl + + 3 times )

                • Select Settings > Preferences... > Margins/Border/Edge > Padding

                • Select the 0 value for the left and right options

                • Select the 5 value 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 & Close button

                Now :

                • Switch to the new tab, mentionned above

                • Hit the F9 key to get the Free Distraction Mode

                • Move at the end of a line containing 81 characters

                • Add the digit 2 => the current line is split and the 2 digit 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 + Tab shortcut to switch to an other application, when using the N++ Distraction Free Mode as hitting again on the F9 key will not return to normal N++ view !

                Simply repeat the Alt + Tab operation to return to N++ first and then hit the F9 shortcut

                1 Reply Last reply Reply Quote 0
                • CoisesC
                  Coises @Thorsten Heuer
                  last edited by

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

                  fml2F 1 Reply Last reply Reply Quote 2
                  • fml2F
                    fml2 @Coises
                    last edited by

                    @Coises Thank you for the quick implementation! If enebaled, it applies to all documets, not just the current one, right?

                    CoisesC 1 Reply Last reply Reply Quote 0
                    • guy038G
                      guy038
                      last edited by guy038

                      Hello, @coises and All,

                      I’ve just tried you last Marginalize plugin and the results are awesome !

                      It’s particularly the case when using the Center the workspace option, in Plugins > Marginalize > Settings, which mimics the Distraction 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

                      1 Reply Last reply Reply Quote 1
                      • CoisesC
                        Coises @fml2
                        last edited by

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

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