ネットワークエディタにスクリプトショートカットメニューを表示する

この記事は[Houdini Advent Calender 2019](https://qiita.com/advent-calendar/2019/houdini)の25日目の記事です。最終日！！

# 初めに
ネットワークエディタ上でShift+Tab(変更可)で独自メニューを表示します。
![houdini_network_editor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/ec2d0d74-e3bc-bd58-cb4a-6842c03c50cb.gif)
キャラ画像はただの趣味です。出さなくてOKです。

Houdini自体にHDA, シェルフ, プリセットと便利ツールが充実しているのでこのような改造をする必要はあんまりないのですが、改造することが好きという方にHoudiniはいじりがいのある楽しいソフトだと思います。

一応作ってみて良いと感じた点を挙げるとすると

 * シェルフに登録するよりもマウスの動きが少ない。
 * スクリプトからノードを作成する際にマウスの位置に作成できる。

まぁシェルフに登録したツールは通常のTABメニューにも表示できるので・・・。
デメリットとしては作成自体に苦労します。コードに記述ミスがあるとHoudiniが延々とエラーメッセージを出力しつづけたりします。

楽しそうと思った方はぜひ。
以下作成方法を書いていきます。使用しているHoudiniは18.0.287でWindows環境です。

公式ドキュメントは[こちら](https://www.sidefx.com/docs/houdini/hom/network.html)。
実際に動いているネットワークエディタのスクリプトが`(Houdiniインストール先)/houdini/python2.7libs/nodegraph.py`にあるので何かあればこちらを参照するのがおすすめです。

# ネットワークエディタ上でキーイベントをフックする

まずはスクリプトファイルを追加します。ドキュメントの方には
`$HOUDINI_USER_PREF_DIR/python2.7libs/nodegraphhooks.py`
`例: C:\Users\(ユーザー名)\Documents\houdini18.0\python2.7libs\nodegraphhooks.py`
を追加すると書かれています。
個人的にはスクリプトの管理をしやすいように別のディレクトリを作成してpythonrc.pyでそのパスを指定しています。

```python:pythonrc.py
import sys
sys.path.insert(0,'スクリプト置き場のパス')
```
どちらでも好きな方で良いかと思います。
nodegraphhooks.pyの動作確認コードは以下になります。

```python:nodegraphhooks.py
#coding:utf-8 
from canvaseventtypes import *

def createEventHandler(uievent, pending_actions):
    """ ネットワークエディタからのイベントをフックする """
    if isinstance(uievent, KeyboardEvent):
        if uievent.eventtype == 'keyhit' and uievent.key=='Shift+Tab':
            uievent.editor.flashMessage(None, 'nodegraphhook!!', 2.0)
    return None, False
```

これでShift+Tabでネットワークエディタにメッセージが表示されます。
![nodegraphhook.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/c40280fb-fcfe-7272-c22e-e61585704de8.gif)
使われていないホットキーを探してそれに対応するコードを書いていくだけでエディタ拡張になります。
頻繁に使用するスクリプト処理があるならここに追加しておくと便利かもしれません。

# メニューを表示する
ここでは[hou.qt.Menu](https://www.sidefx.com/docs/houdini/hom/hou/qt/Menu.html)を使用してショートカットメニューを表示します。
しばらく継続する処理を書くにはnodegraphbase.EventHandlerを継承したクラスを作成しhandleEventという関数で処理していくのが作法のようです。

hou.qt.Menuの元のQMenuはexec_という関数を使って表示することが多いのですがこの関数を使うと警告が表示されることがあるのでshowで表示します。

custom_tabmenu_handler.pyのファイルを追加しています。pythonのパスが通っている場所に置いてください。こだわりがなければnodegraphhooks.pyと同ディレクトリで良いと思います。

```python:nodegraphhooks.py
#coding:utf-8 
from canvaseventtypes import *
from custom_tabmenu_handler import CustomTabMenuHandler

def createEventHandler(uievent, pending_actions):
    """ ネットワークエディタからのイベントをフックする """
    if isinstance(uievent, KeyboardEvent):
        if uievent.eventtype == 'keyhit' and uievent.key=='Shift+Tab':
            # カスタムタブメニュー用のイベントハンドラを返す 
            return CustomTabMenuHandler(uievent), True
    
    return None, False
```

```python:custom_tabmenu_handler.py
#coding:utf-8 
import hou
import nodegraphbase as base
from hutil.Qt import QtGui
from hutil.Qt import QtWidgets

class CustomTabMenuHandler(base.EventHandler):
    """ Shift+Tabで開くカスタムタブメニュー処理 """
    def __init__(self, uievent):
        """ 初期化処理 """
        base.EventHandler.__init__(self, uievent)
        # Menuの作成 
        self.menu = hou.qt.Menu()
        # アクションの登録 
        self.addAction(self.menu, 'test', self.testFunc1)
        self.addAction(self.menu, 'test2', self.testFunc2)
        self.menu.addSeparator()
        self.addAction(self.menu, 'test3', self.testFunc3)
        # Menuの表示
        cursor_pos = QtGui.QCursor.pos()
        self.menu.move(cursor_pos)
        self.menu.show()

    def handleEvent(self, uievent, pending_actions):
        """ イベント処理 """
        # イベント処理を継続する場合はイベントハンドラを返す 
        if self.menu.isVisible():
            return self
        # メニューが消えたらイベント処理を終了する 
        return None

    def addAction(self, menu, name, func):
        """ アクション登録用のヘルパー関数 """
        act = QtWidgets.QAction(name, menu)
        act.triggered.connect(func)
        menu.addAction(act)

    def testFunc1(self):
        """ ネットワークエディタにメッセージを表示 """
        # 初期化で渡したuieventはself.start_uieventで取得可能 
        self.start_uievent.editor.flashMessage(None, 'test1', 2.0)

    def testFunc2(self):
        self.start_uievent.editor.flashMessage(None, 'test2', 2.0)

    def testFunc3(self):
        self.start_uievent.editor.flashMessage(None, 'test3', 2.0)
```

アクションを追加していくにはメンバ関数を追加してaddActionで紐づけしていきます。![nodegraphhook3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/e105d2b9-cedc-0c5b-f57b-bbece86166c9.gif)

# メニューと一緒に画像を表示する
画像だけを表示するウィジェットを作成してhou.qt.Menuと一緒に表示しているだけです。
こんなウィジェットを使用しています。

```python
class PopupImage(QtWidgets.QWidget):
    """ 賑やかし用の画像を表示するだけの存在 """

    image = None

    def __init__(self, parent=None):
        super(PopupImage, self).__init__(parent)
        self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.NoDropShadowWindowHint)
        self.setAttribute(QtCore.Qt.WA_NoSystemBackground)
        self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
        
        if PopupImage.image is None:
            # 画像パスは良い感じに調整してください。
            imagepath = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'images', 'popup_image.png'))
            PopupImage.image = QtGui.QImage()
            PopupImage.image.load(imagepath)

    def paintEvent(self, e):
        # 描画イベント 
        painter = QtGui.QPainter(self)
        painter.drawImage(self.rect(), PopupImage.image, PopupImage.image.rect())
```

# もう少し実用的なサンプル
コードはGitHubに挙げているので詳細はこちらからご確認ください。
https://github.com/towazumi/HouNetworkEditorExtendSample

## OUTという名前のNullノードを追加して色を黒にする
元ネタは[Houdiniで始めるPython](https://qiita.com/70_cg_art/items/46dd9ca06bee4478c495)。
元では選択したノードの名前と色を変更していましたがこちらは直接Nullノードを追加していきます。
気を付けるのはネットワークエディタがSOPレベルにいることを確認してメニューに表示するかチェックすることです。

![nodegraphhook5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/74b345f1-c10b-eeb3-a974-3899ea9721f9.gif)


## 選択中のノードにPythonのテキスト枠と実行ボタンを追加する
元ネタは[Wavefront.mtlファイルをコールバックで読み込む](http://majou.jp/archives/751/)の__2：Stringを利用する手法__です。
この設定をPythonから行います。

![nodegraphhook6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/01b5f8d6-cf89-db3a-83f5-c945f2056344.gif)


## 選択中のノードをPythonコードとして保存する/復元する

元ネタは[[Houdini Snippet] Save Node as Python Code](https://www.youtube.com/watch?v=SY6NyECDwpk)です。
asCodeで生成されるスクリプトに処理を追加して復元するときにその情報を使えるようにしています。

![nodegraphhook7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223831/8873c87e-a878-d19d-77f1-f804ef6e1c1b.gif)

# まとめ
Python3も控えているということでもしかしたら手を出すには危険な時期かもしれませんが自分好みに改造していくのは苦労の分だけ楽しいものがあります。この記事が興味も持ってくれた方の役に立ってもらえれば幸いです。
最後までお読みいただき、ありがとうございました。