Community
    • Login

    Calltip on mouse-over from UDL xml file (Python)

    Scheduled Pinned Locked Moved Help wanted · · · – – – · · ·
    21 Posts 5 Posters 1.7k 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.
    • Khundian TwitchK
      Khundian Twitch @PeterJones
      last edited by Khundian Twitch

      @PeterJones Thanks for the advice!
      I have spent some more time on it, and with your advice got a step closer. I have a searchable dictionary, and got the mouse over working. Still gave to take a look on how to get nicely formatted text instead of the single line with brackets in the calltip.

      I first had a go creating a dictionary with ElementTree because that is standard inside the PythonScript plugin, always nice not having to download extra stuff. But I cannot get the dictionary to respect t the order of the xml, when mouse over the “func” attribute comes before the “name” attribute.

      # Calltip on mouse-over with ElementTree
      from __future__ import print_function
      
      # Import the required modules
      import xml.etree.ElementTree as ET
      from collections import defaultdict
      from pprint import pprint
      
      # Import and parse the data from file
      tree = ET.parse("test.xml")
      root = tree.getroot()
      
      # Create empty dictionary
      data_dict = {}
      
      
      # Imoort the XML data into a Python dictionary
      def etree_to_dict(t):
          d = {t.tag: {} if t.attrib else None}
          children = list(t)
          if children:
              dd = defaultdict(list)
              for dc in map(etree_to_dict, children):
                  for k, v in dc.items():
                      dd[k].append(v)
              d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
          if t.attrib:
              d[t.tag].update(("" + k, v) for k, v in t.attrib.items())
          if t.text:
              text = t.text.strip()
              if children or t.attrib:
                  if text:
                      d[t.tag]["#text"] = text
              else:
                  d[t.tag] = text
          return d
      
      
      data_dict = etree_to_dict(root)
      
      # Print dictionary to console
      # pprint(data_dict, indent=2)
      
      # Clear callbacks
      editor.clearCallbacks()
      
      # Set dwell time
      editor.setMouseDwellTime(500)
      
      
      # Cancel calltip
      def dwell_end(args):
          editor.callTipCancel()
      
      
      # Get word under mouse
      def dwell_start(args):
          pos = args["position"]
          dwell_word = editor.getTextRange(
              editor.wordStartPosition(pos, True), editor.wordEndPosition(pos, True)
          )
          # Print word under mouse to console
          print(dwell_word)
          res = None
          # Search for the the dwell_word in the dictionary
          for sub in data_dict["NotepadPlus"]["AutoComplete"]["KeyWord"]:
              if sub["name"] == dwell_word:
                  res = sub
                  # Show the filter result in console
                  print("The filtered dictionary value is : ", res)
                  # Show dictionary info in calltip
                  editor.callTipShow(editor.getCurrentPos(), res)
                  break
      
      
      console.show()
      print("Calltip on mouse-over with ElementTree installed.")
      
      # Scintilla callback notifications
      show = editor.callback(dwell_start, [SCINTILLANOTIFICATION.DWELLSTART])
      cancel = editor.callback(dwell_end, [SCINTILLANOTIFICATION.DWELLEND])
      
      

      I had a go with xmltodict too, thats a module that has to be downloaded and added to the PythonScript plugin folder. Not ideal, but this module does respect the order of the xml.

      # Calltip on mouse-over with xmltodict
      from __future__ import print_function
      
      # Import the required modules
      import codecs
      from io import open
      import xmltodict
      from pprint import pprint
      
      # Import and parse the data from file
      with open("test.xml", "r", encoding="utf-8") as file:
          data_xml = file.read()
      
      # Create empty dictionary
      data_dict = {}
      
      # Use xmltodict to parse and convert
      data_dict = xmltodict.parse(data_xml, attr_prefix="")
      
      # Print dictionary to console
      # pprint(data_dict, indent=2)
      
      # Clear callbacks
      editor.clearCallbacks()
      
      # Set dwell time
      editor.setMouseDwellTime(500)
      
      
      # Cancel calltip
      def dwell_end(args):
          editor.callTipCancel()
      
      
      # Get word under mouse
      def dwell_start(args):
          pos = args["position"]
          dwell_word = editor.getTextRange(
              editor.wordStartPosition(pos, True), editor.wordEndPosition(pos, True)
          )
          # Print word under mouse to console
          print(dwell_word)
          res = None
          # Search for the the dwell_word in the dictionary
          for sub in data_dict["NotepadPlus"]["AutoComplete"]["KeyWord"]:
              if sub["name"] == dwell_word:
                  res = sub
                  # Show the filter result in console
                  print("The filtered dictionary value is : ", res)
                  # Show dictionary info in calltip
                  editor.callTipShow(editor.getCurrentPos(), res)
                  break
      
      
      console.show()
      print("Calltip on mouse-over with xmltodict installed.")
      
      # Scintilla callback notifications
      show = editor.callback(dwell_start, [SCINTILLANOTIFICATION.DWELLSTART])
      cancel = editor.callback(dwell_end, [SCINTILLANOTIFICATION.DWELLEND])
      
      Alan KilbornA 1 Reply Last reply Reply Quote 1
      • Alan KilbornA
        Alan Kilborn @Khundian Twitch
        last edited by

        @Khundian-Twitch said in Calltip on mouse-over from UDL xml file (Python):

        how to get nicely formatted text instead of the single line with brackets in the calltip.

        Wouldn’t you just insert \r\n into the text string wherever you want the calltip to have a line-break?

        Khundian TwitchK 1 Reply Last reply Reply Quote 2
        • Khundian TwitchK
          Khundian Twitch @Alan Kilborn
          last edited by

          @Alan-Kilborn Thanks for the reply
          I wouldn’t know to be honest. I haven’t used Python that much before. Its not only line breaks though, at the moment it shows the text in brackets too. Picture below also shows the dictionary created with ElementTree not respecting the order of the XML.

          2024-01-19 13_34_12-Window.png

          PeterJonesP 1 Reply Last reply Reply Quote 0
          • PeterJonesP
            PeterJones @Khundian Twitch
            last edited by

            @Khundian-Twitch ,

            You shouldn’t be using the default Python language stringification of the data structure element as the popup, if you want the string to be user-friendly. You should be extracting the elements from that sub-object that are useful to you, in the order you want them to appear, using one of Python’s string-formatting techniques. (That’s nothing specific to Notepad++. Any time you want to take data from an object and present it as a string in Python, you need to format the string to make it look like you want – and if you need help with that, it’s not really a question for here, because that’s generic programming help, not Notepad++ specific.)

            Khundian TwitchK 1 Reply Last reply Reply Quote 2
            • Khundian TwitchK
              Khundian Twitch @PeterJones
              last edited by

              @PeterJones Thanks for the reply and the advice
              I understand that my questions after my initial post are not directly related to Notepad++ anymore. But these nudges in the right direction I got from you is all that I needed,

              I looked into string formatting, and got a step further. Currently it shows only the description of a keyword as calltip, and it only works if there is only 1 Overload entry. I’m going to check how to make it work with multiple hint definitions and the up and down arrows.

              I included a picture and the test.xml as an example in case someone wants to use the script in its current state. All text will be wrapped at max 90 characters, and as stated in the Notepad++ documentation for line breaks use

              

              

              2024-01-30 23_09_32-Window.png

              <NotepadPlus>
              	<AutoComplete language="">
              		<Environment ignoreCase="yes"/>
              		<KeyWord name="ActivateAnim" func="yes">
              			<Overload retVal="" descr='
              &#x0a;------------------------------------------------------------------------------------------
              &#x0a;Plays the specified anim at the specified priority and weight, with optional parameters.
              All arguments except the sequence path are optional.
              This is a low-level animation function that must be used with care.
              Use BlendToAnim to deactivate or transition the animation to a different one.
              &#x0a;------
              &#x0a;Syntax
              &#x0a;------
              &#x0a;actor.ActivateAnim anim sequence path:string
              &#x0a;bIsFirstPerson:0/1
              &#x0a;iPriority:integer
              &#x0a;bStartOver:0/1
              &#x0a;fWeight:float
              &#x0a;fEaseInTime:float
              &#x0a;time sync sequence path:string
              &#x0a;-------
              &#x0a;Example
              &#x0a;-------
              &#x0a;ActivateAnim "Characters\Recoil.kf"
              &#x0a;------------------------------------------------------------------------------------------
              			'>
              				<Param name=""/>
              			</Overload>
              		</KeyWord>
              	</AutoComplete>
              </NotepadPlus>
              
              
              # Calltip on mouse-over with ElementTree
              from __future__ import print_function
              
              # Import the required modules
              import xml.etree.ElementTree as ET
              from collections import defaultdict
              from pprint import pprint
              import textwrap
              
              
              # Import and parse the data from file
              tree = ET.parse("test.xml")
              root = tree.getroot()
              
              # Create empty dictionary
              data_dict = {}
              
              
              # Imoort the XML data into a Python dictionary
              def etree_to_dict(t):
                  d = {t.tag: {} if t.attrib else None}
                  children = list(t)
                  if children:
                      dd = defaultdict(list)
                      for dc in map(etree_to_dict, children):
                          for k, v in dc.items():
                              dd[k].append(v)
                      d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
                  if t.attrib:
                      d[t.tag].update(("" + k, v) for k, v in t.attrib.items())
                  if t.text:
                      text = t.text.strip()
                      if children or t.attrib:
                          if text:
                              d[t.tag]["#text"] = text
                      else:
                          d[t.tag] = text
                  return d
              
              
              data_dict = etree_to_dict(root)
              
              # Print dictionary to console
              # pprint(data_dict, indent=4)
              
              # Clear callbacks
              editor.clearCallbacks()
              
              # Set dwell time
              editor.setMouseDwellTime(500)
              
              
              # Cancel calltip
              def dwell_end(args):
                  editor.callTipCancel()
              
              
              # Get word under mouse
              def dwell_start(args):
                  pos = args["position"]
                  dwell_word = editor.getTextRange(
                      editor.wordStartPosition(pos, True), editor.wordEndPosition(pos, True)
                  )
                  # Print word under mouse to console
                  # print("Mouse over:", dwell_word)
              
                  # Search for the the dwell_word in the dictionary
                  for keyword in data_dict["NotepadPlus"]["AutoComplete"]["KeyWord"]:
                      if keyword.get("name") == dwell_word:
                          nl = "\n"
                          print(dwell_word, "found in dictionary")
                          if keyword.get("func") == ("yes"):
                              # Get name of keyword from dictionary
                              namevalue = keyword.get("name")
                              # Get if keyword is a function, yes/no
                              funcvalue = keyword.get("func")
                              # Get all overload subs
                              overloadvalues = keyword.get("Overload")
                              # Get description of keyword
                              descrvalue = overloadvalues.get("descr")
                              # Wrap the description text for calltip
                              descrvaluewrap = nl.join(
                                  [
                                      nl.join(
                                          textwrap.wrap(
                                              line,
                                              90,
                                              break_long_words=False,
                                              replace_whitespace=False,
                                          )
                                      )
                                      for line in descrvalue.splitlines()
                                      if line.strip() != ""
                                  ]
                              )
              
                              # Print stuff to console
                              # print(namevalue)
                              # print(funcvalue)
                              # pprint(overloadvalues, indent=2)
                              # print(descrvalue)
                              # print(descrvaluewrap)
              
                              # Show the calltip
                              editor.callTipShow(editor.getCurrentPos(), descrvaluewrap)
              
              
              # Open console and show install message
              console.show()
              print("Calltip on mouse-over with ElementTree installed.")
              
              # Scintilla callback notifications
              show = editor.callback(dwell_start, [SCINTILLANOTIFICATION.DWELLSTART])
              cancel = editor.callback(dwell_end, [SCINTILLANOTIFICATION.DWELLEND])
              
              
              1 Reply Last reply Reply Quote 2
              • Khundian TwitchK
                Khundian Twitch
                last edited by Khundian Twitch

                I’ve decided to go with only 1 “Overload” entry per keyword for the time being. I couldn’t find any clear documentation or examples on how to implement them, if anyone has any experience with this… I’m all ears. Unless its to add multiple overloads, this will be the “final” version of the script. A bit funny that it turned out to be “on character” and not “mouse-over”… :)

                Something weird I noticed while testing with the multiple overloads, when the calltip is showing and I use the “alt up” and “alt down” shortcuts it crashes Notepad++ to desktop. Even with the script below that only has 1 “Overload” entry it CTD, clicking on the calltip doesn’t crash btw. I have no idea why it does this, I disabled the parameter hint shortcuts, and recommend it to anyone who wants to use the script below until the cause is found.

                I changed the script to work on a “space” instead of mouse-over, but it can easily be changed to any character you want. I added explanations in the comments on how to edit the script to work with any User Defined Language, and how to change the character that triggers the calltip. Formatted the calltip some more and cleaned the script up, if anyone needs help to get it running… don’t hesistate to ask.

                2024-02-01 22_19_30-Window.png

                <NotepadPlus>
                	<AutoComplete language="">
                		<Environment ignoreCase="yes"/>
                		<KeyWord name="AddAmmoEffect" func="yes">
                			<Overload retVal="(successful:0/1)" descr='
                &#x0a;Adds an ammo effect to an ammo type.
                &#x0a;------
                &#x0a;Syntax
                &#x0a;------
                &#x0a;(successful:0/1) AddAmmoEffect ammo:ref ammoEffect:ref
                &#x0a;-------
                &#x0a;Example
                &#x0a;-------
                &#x0a;AddAmmoEffect Ammo12Ga AmmoEffectBeanBagFatigue 
                			'>
                				<Param name="Form:AnyForm"/>
                			</Overload>
                		</KeyWord>
                	</AutoComplete>
                </NotepadPlus>
                
                # Calltip on any character
                from __future__ import print_function
                
                # Import the required modules
                import xml.etree.ElementTree as ET
                from collections import defaultdict
                import textwrap
                
                # Import and parse the data from file
                tree = ET.parse("PATH/TO/YOUR/AUTOCOMPLETE.XML")
                root = tree.getroot()
                
                # Create empty dictionary
                data_dict = {}
                
                
                # Imoort the XML data into a Python dictionary
                def etree_to_dict(t):
                    d = {t.tag: {} if t.attrib else None}
                    children = list(t)
                    if children:
                        dd = defaultdict(list)
                        for dc in map(etree_to_dict, children):
                            for k, v in dc.items():
                                dd[k].append(v)
                        d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
                    if t.attrib:
                        d[t.tag].update(("" + k, v) for k, v in t.attrib.items())
                    if t.text:
                        text = t.text.strip()
                        if children or t.attrib:
                            if text:
                                d[t.tag]["#text"] = text
                        else:
                            d[t.tag] = text
                    return d
                
                
                data_dict = etree_to_dict(root)
                
                
                # Padding for the calltip
                pad = "----------------------------------------------------------------------"
                
                
                # Clear callbacks
                editor.clearCallbacks()
                
                
                # Get word before the chosen character
                def on_char_add(args):
                    # ---------------------------------------------------------------
                    # The line below makes sure that the code below will only run
                    # when a document is active with your chosen language.
                    # Change YOURLANGUAGENAME to the exact name of your User Defined
                    # Language.
                    # ---------------------------------------------------------------
                    if notepad.getLanguageName(LANGTYPE.USER) == "udf - YOURLANGUAGENAME":
                        # -----------------------------------------------------------
                        # Uncomment the line below, and run the script.
                        # When typing text in a file with the chosen language above,
                        # the "ch" number will show in the console.
                        # Use that number to change the added_char value below,
                        # this will change the character that opens the calltip.
                        # -----------------------------------------------------------
                        # print(args)
                        added_char = args["ch"]
                        if (
                            added_char == 32
                        ):  # Number 32 is the "space" character, change to whatever you want.
                            pos = editor.getCurrentPos() - 1
                            search_word = editor.getTextRange(
                                editor.wordStartPosition(pos, True), editor.wordEndPosition(pos, True)
                            )
                
                            # Search for the the search_word information in the dictionary
                            for keyword in data_dict["NotepadPlus"]["AutoComplete"]["KeyWord"]:
                                if keyword.get("name") == search_word:
                                    nl = "\n"
                                    name_value = keyword.get("name")
                                    overload_values = keyword.get("Overload")
                                    retval_value = overload_values.get("retVal")
                                    descr_value = overload_values.get("descr")
                                    param_value = keyword.get("Overload")["Param"]["name"]
                                    retval_text = "RetVal: " + retval_value
                                    param_text = "Param: " + param_value
                
                                    # Create calltip from all retrieved text
                                    calltip = (
                                        pad
                                        + nl
                                        + name_value
                                        + nl
                                        + pad
                                        + nl
                                        + retval_text
                                        + nl
                                        + pad
                                        + nl
                                        + descr_value
                                        + nl
                                        + pad
                                        + nl
                                        + param_text
                                        + nl
                                        + pad
                                    )
                
                                    # Wrap the text for calltip
                                    calltip_wrap = nl.join(
                                        [
                                            nl.join(
                                                textwrap.wrap(
                                                    line,
                                                    70,
                                                    break_long_words=False,
                                                    replace_whitespace=False,
                                                )
                                            )
                                            for line in calltip.splitlines()
                                            if line.strip() != ""
                                        ]
                                    )
                
                                    # Show the calltip
                                    editor.callTipShow(editor.getCurrentPos(), calltip_wrap)
                
                
                # Open console and show install message
                console.show()
                print("Calltip on any character installed, its safe to close this console.")
                
                
                # Scintilla callback notifications
                editor.callback(on_char_add, [SCINTILLANOTIFICATION.CHARADDED])
                
                
                1 Reply Last reply Reply Quote 1
                • Khundian TwitchK
                  Khundian Twitch
                  last edited by

                  I had some free time and had another go at multiple overloads, but there seems to be a bug with the down arrow in the tooltip / and the alt+down shortcut combo when using PythonScript. Obviously there is nothing wrong with the standard implementation of this feature, only when creating a custom tooltip with PythonScript does this CTD happen.

                  I did a small test with the SCINTILLANOTIFICATION.CALLTIPCLICK and this was the result, position 0 is clicked anywhere but the arrows, position 1 is clicked on the up arrow and position 2 should be clicked on the down arrow… but this results in Notepad++ crashing to desktop.

                  2024-02-02 03_59_55-Window.png

                  Lycan ThropeL mpheathM 2 Replies Last reply Reply Quote 0
                  • Lycan ThropeL
                    Lycan Thrope @Khundian Twitch
                    last edited by

                    @Khundian-Twitch ,
                    Just a note, and I’m not sure if this is relevant with what you’re doing with pythonscript, but the calltip functions are triggered by a delimiter insertion, meaning when you type the keyword, and then click a parens if it is the opening for a function, then the calltip pops up, and you have to use the mouse to click the buttons up or down.

                    What you’re doing, may not make what I just mentioned relevant, since it sounds like you’re past my knowledge, but I thought I might mention in in case it is relevant.

                    1 Reply Last reply Reply Quote 2
                    • mpheathM
                      mpheath @Khundian Twitch
                      last edited by mpheath

                      @Khundian-Twitch said in Calltip on mouse-over from UDL xml file (Python):

                      I did a small test with the SCINTILLANOTIFICATION.CALLTIPCLICK and this was the result, position 0 is clicked anywhere but the arrows, position 1 is clicked on the up arrow and position 2 should be clicked on the down arrow… but this results in Notepad++ crashing to desktop.

                      Exits with

                      Exception Code: c0000094

                      which is a division by zero code. Seems the position key cannot return a value 2 as it fails to get a value.

                      Also a second error

                      Exception Code: c000041d

                      A STATUS_FATAL_USER_CALLBACK_EXCEPTION code.

                      This is what I tested with:

                      from Npp import console, editor, notepad, SCINTILLANOTIFICATION
                      
                      try:
                          callback_registry
                      except:
                          callback_registry = []
                      
                      calltip_wrap = ['\x01 one\n\x02 description', '\x01 two\n\x02 description']
                      
                      if not editor.callTipActive():
                          editor.callTipShow(editor.getCurrentPos(), calltip_wrap[0])
                      
                      def calltip_update(args):
                          console.write(args)
                          pos = editor.getCurrentPos()
                      
                          if editor.callTipActive():
                              if args['position'] == 2:
                                  editor.callTipCancel()
                                  editor.callTipShow(pos, calltip_wrap[0])
                              elif args['position'] == 1:
                                  editor.callTipCancel()
                                  editor.callTipShow(pos, calltip_wrap[1])
                      
                          return True
                      
                      if 'calltip_update' not in callback_registry:
                          callback_registry.append('calltip_update')
                          editor.callback(calltip_update, [SCINTILLANOTIFICATION.CALLTIPCLICK])
                      
                      Khundian TwitchK 1 Reply Last reply Reply Quote 2
                      • Khundian TwitchK
                        Khundian Twitch @mpheath
                        last edited by

                        @mpheath Thanks for taking the time to test, reply and clarify the issue.
                        I guess I’m out of luck. I wonder if it’s something with the PythonScript plugin, when I have some time I’m gonna test with the LuaScript plugin and see if the same happens when creating a calltip.

                        1 Reply Last reply Reply Quote 0
                        • Khundian TwitchK
                          Khundian Twitch
                          last edited by

                          I tested with the LuaScript plugin and exactly the same happens, I guess it has nothing to do with either Python or LUA plugin. I guess its a bug in Scintilla or Notepad++. I’ll post the LUA code I used to test below.

                          function showcalltip(ch)
                          	calltip = "\001 <Click> \002"
                          	pos = editor.CurrentPos
                          	editor:CallTipShow(pos, calltip)
                              return false
                          end
                          
                          function printclick(flag)
                              print(flag)
                              return false
                          end
                          
                          print("Test script running.")
                           
                          npp.AddEventHandler("OnChar", showcalltip)
                          npp.AddEventHandler("OnCallTipClick", printclick)
                          
                          Alan KilbornA 1 Reply Last reply Reply Quote 1
                          • Alan KilbornA
                            Alan Kilborn @Khundian Twitch
                            last edited by

                            @Khundian-Twitch said in Calltip on mouse-over from UDL xml file (Python):

                            I guess its a bug in Scintilla or Notepad++

                            A crash is a pretty big deal; someone should open an official bug report on github.

                            Khundian TwitchK 1 Reply Last reply Reply Quote 2
                            • Khundian TwitchK
                              Khundian Twitch @Alan Kilborn
                              last edited by Khundian Twitch

                              @Alan-Kilborn said in Calltip on mouse-over from UDL xml file (Python):

                              @Khundian-Twitch said in Calltip on mouse-over from UDL xml file (Python):

                              I guess its a bug in Scintilla or Notepad++

                              A crash is a pretty big deal; someone should open an official bug report on github.

                              I agree, but I’m not sure if this qualifies as a bug in Notepad++?
                              The standard implementation of using the arrows to cycle through multiple overloads works fine. Its only when a user starts tinkering with script plugins and creating their own calltips that this crash will occur.

                              1 Reply Last reply Reply Quote 1
                              • Khundian TwitchK
                                Khundian Twitch
                                last edited by

                                I created a bug report on github, guess we’ll see if it qualifies as a bug. Link to report

                                1 Reply Last reply Reply Quote 3
                                • Khundian TwitchK
                                  Khundian Twitch
                                  last edited by

                                  @mpheath Seems it was something in the Notepad++ code, but solved very quickly by the guys on github. Without a doubt the debug info you provided had something to do with that, Thanks again! :)

                                  mpheathM 1 Reply Last reply Reply Quote 2
                                  • mpheathM
                                    mpheath @Khundian Twitch
                                    last edited by mpheath

                                    @Khundian-Twitch Well done! I tested the artifact in PR #14667 that solves the crash issue and so now position 2 is possible for the down arrow of the calltip.

                                    1 Reply Last reply Reply Quote 3
                                    • Khundian TwitchK
                                      Khundian Twitch
                                      last edited by

                                      I have finished the script, if anyone is interested I posted it in the Plugin development forum.
                                      Thanks again to everyone who helped me out!

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