ayon-core/pype/vendor/scriptsmenu/scriptsmenu.py
2018-10-18 13:59:58 +02:00

316 lines
9.2 KiB
Python

import os
import json
import logging
from collections import defaultdict
from .vendor.Qt import QtWidgets, QtCore
from . import action
log = logging.getLogger(__name__)
class ScriptsMenu(QtWidgets.QMenu):
"""A Qt menu that displays a list of searchable actions"""
updated = QtCore.Signal(QtWidgets.QMenu)
def __init__(self, *args, **kwargs):
"""Initialize Scripts menu
Args:
title (str): the name of the root menu which will be created
parent (QtWidgets.QObject) : the QObject to parent the menu to
Returns:
None
"""
QtWidgets.QMenu.__init__(self, *args, **kwargs)
self.searchbar = None
self.update_action = None
self._script_actions = []
self._callbacks = defaultdict(list)
# Automatically add it to the parent menu
parent = kwargs.get("parent", None)
if parent:
parent.addMenu(self)
objectname = kwargs.get("objectName", "scripts")
title = kwargs.get("title", "Scripts")
self.setObjectName(objectname)
self.setTitle(title)
# add default items in the menu
self.create_default_items()
def on_update(self):
self.updated.emit(self)
@property
def registered_callbacks(self):
return self._callbacks.copy()
def create_default_items(self):
"""Add a search bar to the top of the menu given"""
# create widget and link function
searchbar = QtWidgets.QLineEdit()
searchbar.setFixedWidth(120)
searchbar.setPlaceholderText("Search ...")
searchbar.textChanged.connect(self._update_search)
self.searchbar = searchbar
# create widget holder
searchbar_action = QtWidgets.QWidgetAction(self)
# add widget to widget holder
searchbar_action.setDefaultWidget(self.searchbar)
searchbar_action.setObjectName("Searchbar")
# add update button and link function
update_action = QtWidgets.QAction(self)
update_action.setObjectName("Update Scripts")
update_action.setText("Update Scripts")
update_action.setVisible(False)
update_action.triggered.connect(self.on_update)
self.update_action = update_action
# add action to menu
self.addAction(searchbar_action)
self.addAction(update_action)
# add separator object
separator = self.addSeparator()
separator.setObjectName("separator")
def add_menu(self, title, parent=None):
"""Create a sub menu for a parent widget
Args:
parent(QtWidgets.QWidget): the object to parent the menu to
title(str): the title of the menu
Returns:
QtWidget.QMenu instance
"""
if not parent:
parent = self
menu = QtWidgets.QMenu(parent, title)
menu.setTitle(title)
menu.setObjectName(title)
menu.setTearOffEnabled(True)
parent.addMenu(menu)
return menu
def add_script(self, parent, title, command, sourcetype, icon=None,
tags=None, label=None, tooltip=None):
"""Create an action item which runs a script when clicked
Args:
parent (QtWidget.QWidget): The widget to parent the item to
title (str): The text which will be displayed in the menu
command (str): The command which needs to be run when the item is
clicked.
sourcetype (str): The type of command, the way the command is
processed is based on the source type.
icon (str): The file path of an icon to display with the menu item
tags (list, tuple): Keywords which describe the action
label (str): A short description of the script which will be displayed
when hovering over the menu item
tooltip (str): A tip for the user about the usage fo the tool
Returns:
QtWidget.QAction instance
"""
assert tags is None or isinstance(tags, (list, tuple))
# Ensure tags is a list
tags = list() if tags is None else list(tags)
tags.append(title.lower())
assert icon is None or isinstance(icon, str), (
"Invalid data type for icon, supported : None, string")
# create new action
script_action = action.Action(parent)
script_action.setText(title)
script_action.setObjectName(title)
script_action.tags = tags
# link action to root for callback library
script_action.root = self
# Set up the command
script_action.sourcetype = sourcetype
script_action.command = command
try:
script_action.process_command()
except RuntimeError as e:
raise RuntimeError("Script action can't be "
"processed: {}".format(e))
if icon:
iconfile = os.path.expandvars(icon)
script_action.iconfile = iconfile
script_action_icon = QtWidgets.QIcon(iconfile)
script_action.setIcon(script_action_icon)
if label:
script_action.label = label
if tooltip:
script_action.setStatusTip(tooltip)
script_action.triggered.connect(script_action.run_command)
parent.addAction(script_action)
# Add to our searchable actions
self._script_actions.append(script_action)
return script_action
def build_from_configuration(self, parent, configuration):
"""Process the configurations and store the configuration
This creates all submenus from a configuration.json file.
When the configuration holds the key `main` all scripts under `main` will
be added to the main menu first before adding the rest
Args:
parent (ScriptsMenu): script menu instance
configuration (list): A ScriptsMenu configuration list
Returns:
None
"""
for item in configuration:
assert isinstance(item, dict), "Configuration is wrong!"
# skip items which have no `type` key
item_type = item.get('type', None)
if not item_type:
log.warning("Missing 'type' from configuration item")
continue
# add separator
# Special behavior for separators
if item_type == "separator":
parent.addSeparator()
# add submenu
# items should hold a collection of submenu items (dict)
elif item_type == "menu":
assert "items" in item, "Menu is missing 'items' key"
menu = self.add_menu(parent=parent, title=item["title"])
self.build_from_configuration(menu, item["items"])
# add script
elif item_type == "action":
# filter out `type` from the item dict
config = {key: value for key, value in
item.items() if key != "type"}
self.add_script(parent=parent, **config)
def set_update_visible(self, state):
self.update_action.setVisible(state)
def clear_menu(self):
"""Clear all menu items which are not default
Returns:
None
"""
# TODO: Set up a more robust implementation for this
# Delete all except the first three actions
for _action in self.actions()[3:]:
self.removeAction(_action)
def register_callback(self, modifiers, callback):
self._callbacks[modifiers].append(callback)
def _update_search(self, search):
"""Hide all the samples which do not match the user's import
Returns:
None
"""
if not search:
for action in self._script_actions:
action.setVisible(True)
else:
for action in self._script_actions:
if not action.has_tag(search.lower()):
action.setVisible(False)
# Set visibility for all submenus
for action in self.actions():
if not action.menu():
continue
menu = action.menu()
visible = any(action.isVisible() for action in menu.actions())
action.setVisible(visible)
def load_configuration(path):
"""Load the configuration from a file
Read out the JSON file which will dictate the structure of the scripts menu
Args:
path (str): file path of the .JSON file
Returns:
dict
"""
if not os.path.isfile(path):
raise AttributeError("Given configuration is not "
"a file!\n'{}'".format(path))
extension = os.path.splitext(path)[-1]
if extension != ".json":
raise AttributeError("Given configuration file has unsupported "
"file type, provide a .json file")
# retrieve and store config
with open(path, "r") as f:
configuration = json.load(f)
return configuration
def application(configuration, parent):
import sys
app = QtWidgets.QApplication(sys.argv)
scriptsmenu = ScriptsMenu(configuration, parent)
scriptsmenu.show()
sys.exit(app.exec_())