いろんなツールをまとめたツールセットみたいな物をつくってメニューにそれぞれの機能を登録していると、メニューの更新・整理時にだんだんと面倒な感じになってきてコードも荒れてきます。構造データだけ外だしにしてそこからメニューを自動で構築するようにしたら、見通しも良く更新も楽なんじゃないか、と思ったので、夜なべして作りました。
楽でした。
せっかくなので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')
こうなります。menu
というプロパティで各メニューアイテムを表すオブジェクトの配列を指定します。どの要素にもlabel
というプロパティが必須で、これがメニューに表示されるラベルにそのままなります。python
のプロパティがあるとメニュー選択時に呼び出すpythonスクリプトに、mel
のプロパティがあるとメニュー選択時に呼び出すmelスクリプトになります。そこにoption
がtrue
値で存在するとメニュー上でオプションボックスになります。(図のメニュー中で□の表示になってる部分)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
以外にも色々かけます。version
、description
はこのjsonファイルのメタ情報ですが、ついでにabout_dialog
をtrueにしておくと、
ツールにabout
メニューが追加され、選択すると、
こんなダイアログがでます。
また、メニュー構築時に
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で公開しないのは、いろいろな手間とやんごとなき都合によるものです。