Help us understand the problem. What is going on with this article?

【MAYA】メニュー構成をjsonデータで記述して構築する

More than 1 year has passed since last update.

いろんなツールをまとめたツールセットみたいな物をつくってメニューにそれぞれの機能を登録していると、メニューの更新・整理時にだんだんと面倒な感じになってきてコードも荒れてきます。構造データだけ外だしにしてそこからメニューを自動で構築するようにしたら、見通しも良く更新も楽なんじゃないか、と思ったので、夜なべして作りました。
楽でした。:relaxed:
せっかくなのでQiitaで共有します。

実例

こんな感じのjsonデータを用意します。

{
  "name": "すごいツールセット",
  "menu":[
    {
      "label": "python呼び出しサンプル",
      "python": "import os;print('current dir : {}'.format(os.getcwd()))"
    },
    {
      "label": "罫線サンプル"
    },
    {
      "label": "mel呼び出しサンプル",
      "mel": "print(\"hello mel!\\n\");"
    },
    {
      "label": "option メニューサンプル",
      "mel": "print(\"option!\\n\");",
      "option":true
    },
    {
      "label": "サブツール",
      "menu":[
        {
          "label": "python呼び出しサンプルその2",
          "python": "print('hello python2!')"
        }
      ]
    }
  ]
}

これをファイルに保存して、そのパスを今回作ったメニュービルドツールに食わせると、

build_menu('path/to/your/menu.json')

 2018-09-29 1.33.19.png
こうなります。menuというプロパティで各メニューアイテムを表すオブジェクトの配列を指定します。どの要素にもlabelというプロパティが必須で、これがメニューに表示されるラベルにそのままなります。pythonのプロパティがあるとメニュー選択時に呼び出すpythonスクリプトに、melのプロパティがあるとメニュー選択時に呼び出すmelスクリプトになります。そこにoptiontrue値で存在するとメニュー上でオプションボックスになります。(図のメニュー中で□の表示になってる部分)mel/pythonどちらのプロパティもない場合、ディバイダーになります。menuは入れ子にすることができ、その場合メニューではサブメニューになります。

jsonファイルをわざわざ保存するのが面倒臭い?

build_menu({
    'name':'hoge menu',
    'menu':[
        {
            'label':'hoge item',
            'python':'print("hello maya python!")'
        }
    ]
})

直接 pythonのオブジェクトでメニュー構成を指定できるようにもしました。通常はこっちのほうが楽ですね。オブジェクトのフォーマットは上記jsonファイルをjsonモジュールでパースしたものと同じです。

その他設定

{
  "name": "すごいツールセット",
  "version": "0.1",
  "description":"ものすごいツールセットです。",
  "about_dialog":true,
  "top_submenu":true,
  "menu":[
:
:

メニュー名になるname以外にも色々かけます。versiondescriptionはこのjsonファイルのメタ情報ですが、ついでにabout_dialogをtrueにしておくと、
 2018-09-29 1.53.23.png
ツールにaboutメニューが追加され、選択すると、
 2018-09-29 1.53.30.png
こんなダイアログがでます。

また、メニュー構築時に

build_menu('path/to/menu.json', other_menu)

第二引数に別メニューの参照を渡すと、そのメニューのサブメニューとしてメニューを構築できるのですが、top_submenuという設定項目がtrueに設定されていると、その際にツール名でサブメニューを1段階挟みます。設定されていないとそのままmenuの配列で設定されている項目が並びます。実例は面倒なので省略。

ソース

生ソース貼り付けます。シェルフに登録するなり、モジュールとして取り込むなりしてください。
Maya2017 Mac/2018 Macで動作確認。Windowsでは動作確認してませんが、まあ大丈夫でしょう。ところで、Maya2019はまだかいな。

# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
import logging
import json
import pymel.core as pm
_logger = logging.getLogger(__name__)


class _MenuBuilderBase(object):
    main_window = None
    json_source_info = ''
    about_dialog = False
    top_submenu = False

    tool_name = ''
    tool_version = ''
    tool_description = ''

    def __init__(self):
        self._main_window = pm.mel.eval('$tmpVar=$gMainWindow')

    def create_top_menu(self, name):
        return pm.menu(
            'menu_{}'.format(name), parent=self._main_window, label=name, tearOff=True)

    def parse_param(self, json_obj):
        try:
            self.tool_name = json_obj['name']  # 必須
            if 'version' in json_obj:
                self.tool_version = json_obj['version']
            if 'description' in json_obj:
                self.tool_description = json_obj['description']
            if 'about_dialog' in json_obj:
                self.about_dialog = json_obj['about_dialog'] is True
            if 'top_submenu' in json_obj:
                self.top_submenu = json_obj['top_submenu']
        except:
            pm.displayError('menu json parse error : {}'.format(self.json_source_info))

    def create_sub_menu(self, parent_menu, label, tear_off=True):
        return pm.menuItem(parent=parent_menu, label=label, subMenu=True, tearOff=tear_off)

    def create_divider(self, parent_menu, label):
        pm.menuItem(parent=parent_menu, divider=True, dividerLabel=label)

    def create_command(self, parent_menu, label, command, is_python=True, option_box=False):
        source_type = 'mel'
        if is_python:
            source_type = 'python'
        return pm.menuItem(
            parent=parent_menu,
            label=label,
            command=command,
            sourceType=source_type,
            optionBox=option_box
        )

    def build_menu(self):
        # TO BE IMPLEMENTED AT SUB CLASS
        pass

    def create_about_dialog(self, parent_menu):
        if not self.about_dialog:
            return
        command = "pm.confirmDialog(title='{}', message='version: {}\\n\\n{}', button='OK')".format(
            self.tool_name, self.tool_version, self.tool_description)
        _logger.debug(command)
        self.create_command(parent_menu, 'about', command, is_python=True)


class _MenuBuilderV1(_MenuBuilderBase):

    def __init__(self):
        super(_MenuBuilderV1, self).__init__()

    def build_menu(self, json_obj, parent_menu=None):
        self.parse_param(json_obj)
        created_top_menu = None
        if not parent_menu:
            created_top_menu = self.create_top_menu(self.tool_name)
            parent_menu = created_top_menu
        if self.top_submenu and not created_top_menu:
            parent_menu = self.create_sub_menu(parent_menu, self.tool_name)
        try:
            self._build_menu(parent_menu, json_obj['menu'])
        except Exception as ex:
            pm.displayError(
                'error occured during parse menu json: {}\n{}'.format(self.json_source_info, ex.message))

        self.create_about_dialog(parent_menu)

    def _build_menu(self, parent_menu, menu_items):
        for item in menu_items:
            label = item['label']
            option = 'option' in item and item['option'] is True
            if 'python' in item:
                self.create_command(parent_menu, label, item['python'], is_python=True, option_box=option)
            elif 'mel' in item:
                self.create_command(parent_menu, label, item['mel'], is_python=False, option_box=option)
            elif 'menu' in item:
                sub_menu = self.create_sub_menu(parent_menu, label)
                self._build_menu(sub_menu, item['menu'])
            else:
                self.create_divider(parent_menu, label)


def _get_builder(json_obj):
    if 'format_version' in json_obj:
        version = float(json_obj['format_version'])
        if version < 2.0:
            return _MenuBuilderV1()
        pm.displayError('menu builder : no proper version : {}'.format(version))
        return None
    else:
        # バージョン指定がない場合のデフォルト
        return _MenuBuilderV1()


def build_menu_by_json_file(json_path, parent_menu=None):
    try:
        with open(json_path) as fp:
            json_obj = json.load(fp)
            _logger.debug(json_obj)
    except IOError:
        pm.displayError("build_menu error : couldn't load json file : {}".format(str(json_path)))
        return
    builder = _get_builder(json_obj)
    if builder:
        builder.json_source_info = json_path
        builder.build_menu(json_obj, parent_menu)


def build_menu_by_menu_obj(menu_obj, parent_menu=None):
    builder = _get_builder(menu_obj)
    if builder:
        builder.json_source_info = 'from json object'
        builder.build_menu(menu_obj, parent_menu)


def build_menu(_json, parent_menu=None):
    """
    jsonの構造を元にMayaのメニューを構築します

    :param _json: jsonファイルのパス もしくは jsonをパースしたものと等価のpythonObject
    :param parent_menu: 親メニュー
    """
    if isinstance(_json, dict):
        build_menu_by_menu_obj(_json, parent_menu)
    else:
        build_menu_by_json_file(_json, parent_menu)

毎度ソースコードをgitで公開しないのは、いろいろな手間とやんごとなき都合によるものです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした