Calltip on mouse-over from UDL xml file (Python)
-
I don’t know of a way, because Notepad++ does not have an API call that passes they keywords to a plugin.
And unfortunately, Scintilla has a SCI_SETKEYWORD (pythonscript:
editor.setKeyword()
) which Notepad++ uses to set the keyword list (both for normal lexers and for UDL languages), but it does not have the corresponding SCI_GETKEYWORD to retrieve the information.In theory, you could manually read the UDL XML file yourself – but you’d actually have to read all the UDL files (including the old-style userDefineLangs.xml, since that’s where the UDL GUI still puts them) – because the filenames are independent of the UDL names.
I might actually recommend reading the autoCompletion file instead (Notepad++ does allow autoCompletion for UDLs – and I recommend defining one for your UDL), because that XML has attributes which might help you with getting extra information which is not carried in the UDL definition file. (I was actually originally going to suggest just making sure of Notepad++'s auto-completion and parameter-hint information, and thus avoiding the scripting plugin: the parameter hints can give info about the active keyword and any arguments… But it’s done through a keystroke rather than hover-over, and it requires a language whose function arguments have a wrapper like parentheses or something, so
command arg1, arg2
-style languages, without parens, cannot do the hints natively – which means I am guessing it won’t fully meet your needs.) -
@PeterJones Thanks for the reply
Actually I have an autocomplete file set up already, but the language doesn’t use parentheses, square brackets and curly braces in the conventional way. The best solution would be on a space, but I read on the forum that isn’t natively possible. So I started looking into the PythonScript plugin and the Scintilla callbacks, hoping I could find a solution that way. I figured if I get the mouse-over working, it would be possible on a space too.I took your advice and started tinkering a bit myself and made a bit of progress. While far from finished the script below shows its technically possible, Current script iterates through all the functions and when the mouse-over word is the same as the one being checked in the XML file, it shows the return value, name, description and parameters of the keyword. This works with a single or a couple entries in the xml. But my full autocomplete file has close to 3000 entries, would need to be really lucky to see the calltip this way.
I will add the test.xml too, with the current settings in the script, this file should be in the root notepad folder if anyone wants to tinker with the script.
# Calltip on mouse-over script from __future__ import print_function import xml.etree.ElementTree as ET tree = ET.parse("test.xml") root = tree.getroot() # Clear Callbaks 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) ) # Find keyword names and if function or not for keyword in root.iter("KeyWord"): func = keyword.get("func") kname = keyword.get("name") # print (name, func) # Find return values and descriptions for overload in root.iter("Overload"): descr = overload.get("descr") retval = overload.get("retVal") # print (retval, descr) # Find parameter names for param in root.iter("Param"): pname = param.get("name") # print (pname) # Show Calltip if dwell_word == kname: editor.callTipShow(editor.getCurrentPos(), (retval, kname, descr, pname)) # print (editor.callTipShow, pos) else: # Fail message in console when dwell_word doesn't match print("Fail") console.show() print("installing callback.") show = editor.callback(dwell_start, [SCINTILLANOTIFICATION.DWELLSTART]) print("Result is ", show) cancel = editor.callback(dwell_end, [SCINTILLANOTIFICATION.DWELLEND]) print("Result is ", cancel)
<NotepadPlus> <AutoComplete language=""> <Environment ignoreCase="yes"/> <KeyWord name="Abs" func="yes"> <Overload retVal="Float" descr="When given a number, returns the absolute value of the number."> <Param name="Float:Float"/> </Overload> </KeyWord> </AutoComplete> </NotepadPlus>
-
@Khundian-Twitch said in Calltip on mouse-over from UDL xml file (Python):
This works with a single or a couple entries in the xml. But my full autocomplete file has close to 3000 entries, would need to be really lucky to see the calltip this way.
I think you are duplicating effort too much in your current script: right now, every time you hover, it iterates over the whole XML, which is hugely inefficient – the UDL’s autoCompletion XML isn’t changing, so you want to just process it once at the beginning. What I would do would be to create a nested dictionary structure, where the key of the top-level dictionary is the word (in this case, the keyword/function name), and then you store the other info in keys under that. That all gets done during script initialization. Then, in the hover-over event, you just need to grab the word you’re hovering over, and then your
# Show Calltip
if condition would just beif dwell_word in dataStructure
The datastructure I would use would look something like
dataStructure = { 'Abs': [ { 'retVal': 'Float', 'descr': 'When given a number, ...', 'params' : [ 'float:float' ] } ], 'FuncWithOptionalParams': [ { 'retVal': 'default', 'descr': 'This overload has 0 params', 'params': [] }, { 'retVal': 'alt1', 'descr': 'This overload has 1 params', 'params': ['in1: integer'], }, { 'retVal': 'alt2', 'descr': 'This overload has 2 params', 'params': ['in1: integer', 'in2: float'], } ] } }
Then when you hovered over the first word on each line below, it would run the simpler callback…
Abs -3.14159 DoesNotExist FuncWithOptionalParams
The middle line would not find ‘DoesNotExist’ in
dataStruct
, so it wouldn’t tooltip. The first and third lines would find the matching key in yourdataStruct
dictionary, so could go on to create an appropriate tooltip –if dwell_word in dataStruct: str = "" for overload in dataStruct[dwell_word]: str += overload['descr'] + "\n" # ... create tooltip from string here
-
@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])
-
@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? -
@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. -
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.)
-
@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


<NotepadPlus> <AutoComplete language=""> <Environment ignoreCase="yes"/> <KeyWord name="ActivateAnim" func="yes"> <Overload retVal="" descr=' 
------------------------------------------------------------------------------------------ 
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. 
------ 
Syntax 
------ 
actor.ActivateAnim anim sequence path:string 
bIsFirstPerson:0/1 
iPriority:integer 
bStartOver:0/1 
fWeight:float 
fEaseInTime:float 
time sync sequence path:string 
------- 
Example 
------- 
ActivateAnim "Characters\Recoil.kf" 
------------------------------------------------------------------------------------------ '> <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])
-
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.
<NotepadPlus> <AutoComplete language=""> <Environment ignoreCase="yes"/> <KeyWord name="AddAmmoEffect" func="yes"> <Overload retVal="(successful:0/1)" descr=' 
Adds an ammo effect to an ammo type. 
------ 
Syntax 
------ 
(successful:0/1) AddAmmoEffect ammo:ref ammoEffect:ref 
------- 
Example 
------- 
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])
-
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.
-
@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.
-
@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 value2
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])
-
@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. -
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)
-
@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.
-
@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. -
I created a bug report on github, guess we’ll see if it qualifies as a bug. Link to report
-
@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! :)
-
@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. -
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!