SlicerEditor improvement inquiry

Following from the last PW, there is now a functional prototype of SlicerEditor based on open-source monaco editor. If you would like to give it a try, please use the “working” branch from:

You can use the provided code example, highlight and hit run to execute in the python console.

Currently the plan is write these files directly to disk, using a node selector/creator. However, I would like the scene to be aware of these files, and if possible become part of the scene tree (like the Texts node) and the I/O is handled like any other object in the scene tree through the regular save or export as options.

How can we achieve this? should we create a MRML object type called Scripts (similar to Text object)? Can we do this through as an extension, or does it require changes to the core?

What would be the alternative to accomplish scene integration?

3 Likes

For reference:

https://discourse.slicer.org/t/support-python-text-highlighting-in-text-module/34511

and

https://projectweek.na-mic.org/PW41_2024_MIT/Projects/SimpleEditorForPythonScripting/

Regarding integration with the scene, I suggest we start just by using Text nodes, since those are already exist. I could make sense to have a subclass that does some “scripty” things, but I don’t think that’s needed now. We could have a node attribute that flags script nodes as a kind of text node to be handled specially, e.g. to always save in a file rather than in the xml.

But I’m still not convinced that mixing code and data is a good architecture. Usually we try to have scripts that are generic and can be applied to any data rather than having data-specific scripts. Perhaps for demos or training though having everything in the scene can make sense so I suggest we play with it and maybe add more features before deciding about any changes to the core.

Also can we put this in the extension manager now so it’s easier for people to discover and try?

1 Like

(Also, with the Text node the text data can already be in a dedicated file that’s pointed to by the mrml xml, so if we just make sure to set them up that way from the SlicerEditor code then we’ll get what I believe is the behavior you are looking for.)

1 Like

Wouldn’t this conflict with the regular Texts module? Would be possible to distinguish nodes bearing python scripts vs regular text? I am concerned it will clash and the right-click “Edit Properties” would open the python script in the Texts module as opposed to SlicerEditor.

I think that’s the function of the script repository, ie., to provide generic examples that applies to large number of people. Here I think the intention is exactly what you desribed, a script specific to the data the user is processing. I think for the reproducibility and retention it is important to keep these things together. But I suspect, you are right we have to experiemnt some…

Once few remaining things are sorted out, yes, that’s the intention.

1 Like

Yes, these are doable with the existing infrastructure. The qMRMLNodeComboBox can be configured to only show nodes with specific attributes, so if we used that to mark text nodes as scripts then only those would be selectable in the editor. Similarly we could have a subject hierarchy plugin that checks the node attribute and offers an ‘edit as script’ menu option.

The more I think about it, I do like the idea of capturing the script as a way of documenting what was done in processing the data in the scene. Usually that information is ephemeral or lost in the log files. Let’s see how it works out in a few specific cases.

1 Like

Right now only vtkMRMLTextNode are displayed when you try to import a node from the scene, I’m guessing with qMRMLNodeComboBox only nodes with a python attribute would be displayed? That would be really nice. How do you create a subject hierarchy plugin?

You could provide a scripted file reader, something like this one that registers the .py extension and loads it into a text node and then uses the SetAttribute method to set TextType to be PythonScript.

Then in the Editor combo box you can use addAttribute to filter the nodes that appear.

Then it’s also possible to put in a subject hierarchy plugin that checks the FileType attribute and adds an option to use the SlicerEditor module. This documentation should help, and you can see how it’s used in practice here.

I actually want a slightly different behavior. I want the script object behave more like Volumes node. I.e., scene is aware of it, but data is written to / read from a file, instead of the scene.

So for script object, the Text node can keep its attributes (like it is modified etc), but the actual script is written to file.

Yes, you can do it with this parameter, which can be set in the custom reader for the scripts:

Hi everyone,

I wanted to share some progress and seek advice on a couple of issues I’m encountering with the SlicerEditor module.

Current Progress:

  1. Subject Hierarchy Plugin: I’ve added a subject hierarchy plugin. When such a node is right-clicked, a new menu option for saving .py files is now available. This will be updated to override the existing “Export to file…” option, allowing .py files to be saved directly.
  2. Mime Type Attribute: I successfully added the mimetype attribute to vtkMRMLText nodes from .py files dropped into the Slicer window for import.
  3. PyFile Reader/Writer Module: I’ve added a module that reads and writes .py files to disk

Issues Encountered:

  1. Recording Executed Code: One of the core functions of the SlicerEditor module is currently lacking—the ability to record the code executed when the run button is clicked. Is there a way to programmatically “paste” the code from the editor into the interactor within the module? Or is there an alternative method to run the code that would allow users to use the up button (at the interactor) to see what has already been executed?
  2. Attribute Extraction for Imported .py Files: The canOwnSubjectHierarchyItem method isn’t extracting the set attributes of the imported .py file as expected. I’m trying to use this to override the default text node icon, but the attributes return None.

Here’s a snippet of the relevant code:

def canOwnSubjectHierarchyItem(self, itemID):
    pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
    shNode = pluginHandlerSingleton.subjectHierarchyNode()
    node = shNode.GetItemDataNode(itemID)

    if node:
        print("Node found:", node.GetName())
        mimetype = node.GetAttribute("mimetype")
        fileType = node.GetAttribute("fileType")
        print("Node mimetype:", mimetype, "fileType:", fileType)
        if mimetype == "text/x-python" or fileType == "python":
            return 1.0
    else:
        print("No node found for itemID:", itemID)
    return 0.0

def icon(self, itemID):
    pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
    shNode = pluginHandlerSingleton.subjectHierarchyNode()
    node = shNode.GetItemDataNode(itemID)

    if node and (node.GetAttribute("mimetype") == "text/x-python" or node.GetAttribute("fileType") == "python"):
        return self.fileIcon

    # Check the file extension of the storage node
    storageNodeID = node.GetStorageNodeID()
    if storageNodeID:
        storageNode = slicer.mrmlScene.GetNodeByID(storageNodeID)
        if storageNode and storageNode.GetFileName().lower().endswith('.py'):
            return self.fileIcon

    return qt.QIcon()

To recreate this issue, you can install the working version-2 branch of the SlicerEditor and drag and drop a .py file with some code into the Slicer data window to import the file.

I appreciate all the help so far and look forward to further developing this module with your assistance!

Best regards,
Oshane

Yeah, that’s a quite significant problem. While the code do get executed, and created python objects are accessible in the python console, not seeing the lines of code run, or history of them (with up arrow) is very confusing.

Given that you can paste code into Python console from outside, I thought maybe it is possible to do exactly that, stream the highlighted code as if it is copy/pasted? @pieper @lassoan @jcfr

I think this is the only remaining significant obstacle before we add it to the extension catalogue for people to try…

1 Like

You can print anything to the console using print... methods, for example: slicer.app.pythonConsole().printOutputMessage("something")

But maybe what we miss is that when multiple lines of code is copy-pasted then that does not get added to the command history (you cannot re-run by up-arrow and Enter). I think these multiline commands are not added to the history because there are some limitations in the console about running multi-line code and returning a value (the Python interpreter can either run single command and return a value that can be displayed; or run multiline commands but not return a value). You can see this limitation if you type multiple lines of code (using Shift+Enter to start a new line without executing the code) and only the first line of the code will be executed. It should be possible to improve the behavior and automatically switch to multiline execution. We could then keep very long code snippets in the command history.

I agree, it may not be the best idea.

There is code, data, and documentation; and indded it is somewhat inconvenient to deal with all three separately. But instead of bundling data with code, I would rather bundle code with documentation (as they are much more similar) and make it very easy to run code snippets from documentation. For example, we could use custom URL protocol to run a code snippet that is shown in a tutorial document in the browser, by a single click.

But Jupyter does exactly this (bundling code and documentation), so it is not clear for me why we do not just use notebooks.

There is also VS Code. Step-by-step debugging is really useful. For that, you need to have your code in a .py file that is opened in VS Code. The only inconvenience there that you need to run the code by calling execfile('path/to/myscript.py') in the debug console in VS Code instead of just pressing F5. But this can be addressed by adding a small custom VS Code extension (that registers a new command, which executes the currently edited file in the debug console).

1 Like

I agree, Jupyter notebooks are great. If people are comfortable with it, and know how to set it up correctly, they should use it. Nor, this editor is meant to be a replacement for IDEs.

The editor is there for recording things that cannot be recorded in Slicer easily. The typical example is a custom segmentation pipeline that involves thresholding and smoothing, parameters of which is get lost if all you save the data. You can of course do this through the Jupyter notebooks, but the format is too verbose making it harder to find it by opening the notebook in a text editor.

So this is just another way of doing things, it is not so uncommon in Slicer to have tools with overlapping functionalities. So I am not sure why this has to be either/or situation.

1 Like

OK, this may be important.

Would you like kind of “macro” functionality? To be able to easily specify (preferably record) a few processing steps and able to rerun it on various data?

Would users want to save their macros? Share macros with others? Then it really sounds like we would need to make it simpler to create, edit, run, and debug scripted modules and extensions.

We could also create a simple, Pythonic “SlicerMacros” library, which would allow using Slicer in a macro-like language without requiring anyone to know anything about Slicer, MRML, VTK, Qt, etc. SlicerMacros could be in an extension so that it could be continuously updated based on user requests. For example, the “macro” functions could work like this:

import SlicerMacros as sm
vol = sm.load_volume('path/to/volume.nrrd')
seg = sm.create_segmentation(vol)
seg1 = sm.create_segment(seg, "test")
sm.segment_threshold(seg1, vol, 200, 323)
sm.segment_smooth(seg1, 'joint', 0.3)
sm.save_segmentation(seg, 'path/to/segmentation.seg.nrrd')

Yes, definitely! Once we have figured out the nuts and bolts of the editor, I was thinking on going a direction where people can import generic scripts for various tasks and customize if for their data. This sound very similar (if not better than that).

This sounds very useful. But are you thinking of this as a package like slicerio which can be used without slicer. Or will this code be executed inside the Slicer?

We do try to teach little bit of Python programming in our workshops, and an example like above will motivate people a lot more. The whole interacting with MRML/Qt objects business adds a significant complexity to even the simplest tasks, so abstraction like this would be great

1 Like

I’ve solved the issue with the icon, but I’m still having some trouble using the “Edit Properties” option when a python text node is clicked. The SlicerEditor opens, but the code does not appear in the editor. This only occurs if the SlicerEditor module has not been called i.e. after startup before the module is loaded. Here’s my code for edit properties where I’ve been trying to a few things to solve the problem:

    def editNodeInSlicerEditor(self, node):
        pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
        pluginHandlerSingleton.pluginByName("Default").switchToModule("SlicerEditor")

        # Allow some time for the module to fully load
        slicer.app.processEvents()
        time.sleep(0.5)  # Adjust the sleep time if necessary

        editorWidget = slicer.modules.slicereditor.widgetRepresentation().self()
        code = node.GetText().replace('\\', '\\\\').replace('`', '\\`').replace('"',
                                                                                '\\"')  # Escape backslashes, backticks, and double quotes

        # Define the JavaScript code as a string
        jsSetEditorContent = f"""
            function setEditorContent() {{
                if (window.editor) {{
                    window.editor.getModel().setValue(`{code}`);
                }} else {{
                    setTimeout(setEditorContent, 500);
                }}
            }}
            setEditorContent();
        """

        # Use evalJS to execute the JavaScript function
        editorWidget.editorView.evalJS(jsSetEditorContent)

    def editProperties(self, itemID):
        print("Edit Properties action triggered")
        node = self.subjectHierarchyNode.GetItemDataNode(itemID)
        if node:
            self.editNodeInSlicerEditor(node)

This runs perfectly fine if the module has already been selected manually and the user tries to “Edit Properties” of the python text node after.

I’ve updated the version-2 branch of the repo:

Simply install and drag and drop a python file from disk to slicer to test.

Oshane

**is this because we are using a slicer.qSlicerWebWidget() ?

Nevermind…problem solved. I had to properly give the slicer.qSlicerWebWidget() and JS enough time to load:

    def editNodeInSlicerEditor(self, node):
        editorModule = slicer.modules.slicereditor
        slicer.util.selectModule(editorModule.name)

        editorWidget = slicer.modules.slicereditor.widgetRepresentation().self()
        code = node.GetText().replace('\\', '\\\\').replace('`', '\\`').replace('"',
                                                                                '\\"')  # Escape backslashes, backticks, and double quotes

        def setEditorContent():
            jsSetEditorContent = f"""
                (function setEditorContent() {{
                    if (window.editor && window.editor.getModel()) {{
                        window.editor.getModel().setValue(`{code}`);
                    }} else {{
                        setTimeout(setEditorContent, 100);
                    }}
                }})();
            """
            editorWidget.editorView.evalJS(jsSetEditorContent)

        # Adding a slight delay to ensure the editor is initialized
        qt.QTimer.singleShot(500, setEditorContent)

    def editProperties(self, itemID):
        print("Edit Properties action triggered")
        node = self.subjectHierarchyNode.GetItemDataNode(itemID)
        if node:
            self.editNodeInSlicerEditor(node)

Yes, I think it would be great to be able to download and use (and share) handy scripts. It would also be nice if we could integrate it with the UI a bit. I.e. in the script repository we have some examples with hard-coded node names and text telling people to replace the string (a common source of confusion it seems). Instead maybe slicer.util.getNode could prompt to select a node when run from the editor or console and doesn’t recognize the input string.

1 Like