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 878 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.
    • h-jangraH
      h-jangra @fml2
      last edited by

      @fml2 Yes, currently gq in visual line mode works with custom width. I haven’t thought about that and didn’t update wrap command.

      1 Reply Last reply Reply Quote 0
      • 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