Skip to content

Conversation

zas
Copy link
Collaborator

@zas zas commented Apr 17, 2025

Summary

  • This is a…
    • Bug fix
    • Feature addition
    • Refactoring
    • Minor / simple change (like a typo)
    • Other
  • Describe this change in 1-2 sentences:

Problem

  • JIRA ticket (optional): PICARD-XXX

Solution

Action

Additional actions required:

  • Update Picard documentation (please include a reference to this PR)
  • Other (please specify below)

@zas
Copy link
Collaborator Author

zas commented Apr 17, 2025

There's room for improvements but that's the idea.

@zas zas requested review from phw and rdswift April 17, 2025 11:10
@zas
Copy link
Collaborator Author

zas commented Apr 17, 2025

Of course, we can add longdesc, and I guess we can get rid of most constants (like CALCULATED_TAGS, PRESERVED_TAGS,...) and just use ALL_TAG_VARS. I expect it could simplify code in many places, and it is easier to extend.

Instead of is_* properties we could perhaps use flags (though not sure it will simplify anything).

Copy link
Collaborator

@rdswift rdswift left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My long-term vision was to also allow plugins to register their tags and variables for autocompletion and tooltips. The initial thinking was to hold the information in a class like:

class PluginTagsAndVariables():
    _tags_and_variables = {}
    """Dictionary containing the tags and/or variables for a plugin along with
    the tooltip description by plugin identifier."""

    @classmethod
    def get_plugins(cls):
        """Returns a list of the names of the plugins with tags and/or variables
        registered for autocompletion.

        Return:
            list: List of registered plugin names.
        """
        return [x for x in cls._tags_and_variables.keys()]

    @classmethod
    def get_plugin_tags(cls):
        """Yields tag and variable names for all plugins registered for autocompletion.

        Yields:
            str: Tag or variable name.
        """
        for items in cls._tags_and_variables.values():
            yield from items.keys()

    @classmethod
    def get_plugin_tags_and_descriptions(cls):
        """Yields tuples of tag and variable names along with their descriptions for all plugins
        registered for autocompletion.

        Yields:
            tuple: Tuple of (tag or variable name, description).
        """
        for tags in cls._tags_and_variables.values():
            for tag, desc in tags.items():
                yield (tag, desc)

    @classmethod
    def add_tag(cls, plugin: str, tag: str, desc: str):
        """Add a tag or variable name to a plugin for autocompletion.

        Args:
            plugin (str): ID of the plugin. This must be the name of the plugin python file (or the
            plugin zip file) without the extension, not the PLUGIN_NAME constant within the plugin.
            tag (str): Tag or variable name to add.  Cannot contain any spaces.
            desc (str): Description of the tag or variable suitable for a tooltip.
        """
        _plugin = plugin.strip()
        _tag = tag.strip()
        _desc = desc.strip()
        if not _plugin or not _tag or ' ' in _tag:
            return
        if not _desc:
            _desc = _('No description provided')
        if _plugin not in cls._tags_and_variables:
            cls._tags_and_variables[_plugin] = set()
        cls._tags_and_variables[_plugin][_tag] = f"{desc} [plugin]"
        # TODO: Consider adding the plugin name to the description.

    @classmethod
    def register_plugin(cls, plugin: str, tags=None):
        """Register a plugin and its tags and variables for autocompletion.

        Args:
            plugin (str): ID of the plugin. This must be the name of the plugin python file (or the
            plugin zip file) without the extension, not the PLUGIN_NAME constant within the plugin.
            tags (iterable, optional): Iterable of tuples (tag/variable, description) register. Defaults to None.
        """
        if not plugin.strip():
            return
        if not tags:
            tags = set()
        if isinstance(tags, str):
            tags = [(tags, '')]
        try:
            for tag in tags:
                cls.add_tag(plugin, tag[0], tag[1])
        except TypeError:
            pass

    @classmethod
    def de_register_plugin(cls, plugin: str):
        """De-register a plugin and remove all of its related tags and variables from the scripting
        autocompletion.

        Args:
            plugin (str): ID of the plugin. This must be the name of the plugin python file (or the
            plugin zip file) without the extension, not the PLUGIN_NAME constant within the plugin.
        """
        cls._tags_and_variables.pop(plugin.strip(), None)

When a plugin was enabled, it could either register its tags and variables directly (which I had working in a copy of the 2.x branch), or this could be part of the registration process (perhaps through another constant in the plugin header).

when a plugin is disabled, I planned to have some extra code in the picard/ui/options/plugins.py module's save() method to de-register any plugins not in the enabled_plugins setting.

There's probably a cleaner way of handling this, but these were my initial thoughts.

@rdswift
Copy link
Collaborator

rdswift commented Apr 17, 2025

What would be really nice (in my opinion) would be to provide a description (perhaps the short description) along with the tag/variable name in the autocompletion list. Trouble is, I suspect that would be a huge effort and may not be worth doing.

@zas
Copy link
Collaborator Author

zas commented Apr 17, 2025

My long-term vision was to also allow plugins to register their tags and variables for autocompletion and tooltips. The initial thinking was to hold the information in a class like:

This PR makes it actually much easier because it makes things more dynamic. We could even have plugin tags/vars marked with a plugin prefix (something like %pluginid.tag% %pluginid._var%) in order to avoid conflicts between plugins and/or main code.

@zas
Copy link
Collaborator Author

zas commented Apr 17, 2025

What would be really nice (in my opinion) would be to provide a description (perhaps the short description) along with the tag/variable name in the autocompletion list. Trouble is, I suspect that would be a huge effort and may not be worth doing.

We could at least provide a dynamic help for tags & variables as we do with functions.

@rdswift
Copy link
Collaborator

rdswift commented Apr 17, 2025

I was just thinking, if you like I can look after adding the long descriptions (copying them from the Picard User Guide as @phw suggested) where appropriate when I start working on #2636 again. They willl be pretty much related to that PR anyway because this one doesn't use them at all.

@zas zas marked this pull request as ready for review April 18, 2025 09:32
@zas zas changed the title Introduce TagVar Introduce TagVar & TagVars classes Apr 18, 2025
@zas zas requested a review from rdswift April 18, 2025 09:46
rdswift
rdswift previously approved these changes Apr 18, 2025
Copy link
Collaborator

@rdswift rdswift left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks good to me. It should make maintenance and working with tags and variables easier. Nice.

@rdswift
Copy link
Collaborator

rdswift commented Apr 21, 2025

We could even have plugin tags/vars marked with a plugin prefix (something like %pluginid.tag% %pluginid._var%) in order to avoid conflicts between plugins and/or main code.

This sounded good initially, but the more I think about it I'm not sure I fully understand what you mean.

Are you suggesting that users will need to prefix any variables from a plugin with the plugin ID when using them in scripts? I hope not, because that would break any scripts using plugins like "Additional Artists Variables". Further, I don't think a plugin should change the definition of an existing tag or variable (although it could change the value).

I do like the idea of storing the new tags and variables created by a plugin in the TagVars to provide consistency for autocompletion and ensuring that duplicates (with different definitions) are not created. I have some thoughts on extending this, but will hold off creating a PR for discussion until this one has been merged.

@rdswift
Copy link
Collaborator

rdswift commented Apr 21, 2025

When this change is merged, are there plans to also include it in the 2.x branch?

@zas
Copy link
Collaborator Author

zas commented Apr 21, 2025

Are you suggesting that users will need to prefix any variables from a plugin with the plugin ID when using them in scripts?

Well, I didn't really think about it, but I was thinking about a way for plugins to add variables without risking to be in conflict with another plugin doing the same, they would still be able to modify any existing tags.

@zas
Copy link
Collaborator Author

zas commented Apr 21, 2025

When this change is merged, are there plans to also include it in the 2.x branch?

Perhaps not easy to do, but if it's possible why not.

Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zas This is a good move. The ta definition requires a bit more code, but usage is simplified. Some comments for discussion, but otherwise this looks good.

I think it would be good to update VariableScriptToken.get_tooltip in scripttextedit.py already to make use of the new longdesc (even if that is not yet filled for any tag). This requires some helper to get the long description from a tag name, which would make the API more complete.

@rdswift
Copy link
Collaborator

rdswift commented Apr 22, 2025

I think it would be good to update VariableScriptToken.get_tooltip in scripttextedit.py already to make use of the new longdesc (even if that is not yet filled for any tag). This requires some helper to get the long description from a tag name, which would make the API more complete.

@phw, I actually have this coded up in a local branch waiting for this PR to be merged so I can rebase and update #2636 (or open a new PR). The approach I took was to add another method called display_tooltip() to the TagVars class, and then used that in scripttextedit.py. @zas, would you like me to create a pull request to add this to your branch, or between you and @phw do you want to merge this without it and then add it in with my PR?

The code looks like:

TEXT_NOTES = N_('**Notes:**')
TEXT_READ_ONLY = N_('read-only')
TEXT_FROM_FILE = N_('provided from the file')
TEXT_NAMING_ONLY = N_('available in naming scripts only')
TEXT_NOT_FOR_SCRIPTING = N_('not available for use in scripts')
TEXT_CALCULATED = N_('calculated variable')
TEXT_NOT_FROM_MB = N_('not provided from MusicBrainz data')
TEXT_NO_DESCRIPTION = N_('No description available.')


    # This is the new method in the TagVars class.
    def display_tooltip(self, name):
        tagdesc = None

        if name and name.startswith('_'):
            name = name.replace('_', '~', 1)

        if ':' in name:
            name, tagdesc = name.split(':', 1)

        item: TagVar = self._name2item.get(name, None)
        title = _(item.longdesc) if item and item.longdesc else _(TEXT_NO_DESCRIPTION)

        notes = []
        if item:
            if item.is_preserved:
                notes.append(_(TEXT_READ_ONLY))
            if item.is_calculated:
                notes.append(_(TEXT_CALCULATED))
            if item.is_file_info:
                notes.append(_(TEXT_FROM_FILE))
            if not item.is_script_variable:
                notes.append(_(TEXT_NOT_FOR_SCRIPTING))
            if not item.is_from_mb and not item.is_file_info:
                notes.append(_(TEXT_NOT_FROM_MB))
        if notes:
            title += f"\n\n{_(TEXT_NOTES)} {'; '.join(notes)}."

        if markdown is not None:
            title = markdown(title)

        if name and name.startswith('~'):
            name = name.replace('~', '_', 1)

        if tagdesc:
            return f"<p><em>%{name}%</em> [{tagdesc}]</p>{title}"
        else:
            return f"<p><em>%{name}%</em></p>{title}"

@phw
Copy link
Member

phw commented Apr 22, 2025

@rdswift That is great. I guess in this case we could also separate it and wait for this PR to merge first. Just that we don't forget about this part :)

@rdswift
Copy link
Collaborator

rdswift commented Apr 22, 2025

Just that we don't forget about this part :)

After spending a few days adding all the long descriptions, updating the tooltips displayed, and writing tests, it is really unlikely I'm going to forget about it. 😜

@zas
Copy link
Collaborator Author

zas commented Apr 22, 2025

@phw I implemented your suggestions (I think), most notably the TagVar.script_name() suggestion to drop the replace(), that's a good move.

@zas
Copy link
Collaborator Author

zas commented Apr 22, 2025

Just that we don't forget about this part :)

After spending a few days adding all the long descriptions, updating the tooltips displayed, and writing tests, it is really unlikely I'm going to forget about it. 😜

I think this PR will be ready to merge soon, so you'll be able to work on your PR for long descriptions :)

@zas zas requested a review from phw April 22, 2025 20:01
Copy link
Member

@phw phw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. This looks good to go to me

@zas zas merged commit bfb0020 into metabrainz:master Apr 23, 2025
51 checks passed
@zas zas deleted the tagvar branch April 23, 2025 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants