【Unity】【UI Toolkit】コピー&ペーストなどのショートカットをハンドリングするCommand Eventの使い方

UnityのUI Toolkitでコピー&ペーストなどのショートカットをハンドリングするCommand Eventの使い方についてまとめました。

Unity 2022.2.19

Command Eventとは

Command Eventをハンドリングすると、キーボードショートカットやメニューの実行により、コピーやペーストなどが行われた時の処理を記述できます。
このイベントはUnityEditor専用のイベントとなり、ランタイムでは使えないのでご注意ください。

コピー&ペーストをハンドリングする

それでは実際にCommand Eventを使ってコピー&ペーストをハンドリングします。

Command Eventをハンドリングするには、ValidateCommandEventExecuteCommandEventの二つのイベントについてのコールバックを登録します。

using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    private readonly Color[] _sourceColors =
    {
        Color.Lerp(Color.red, Color.black, 0.25f),
        Color.Lerp(Color.green, Color.black, 0.25f),
        Color.Lerp(Color.blue, Color.black, 0.25f),
        Color.Lerp(Color.yellow, Color.black, 0.25f),
        Color.Lerp(Color.cyan, Color.black, 0.25f),
        Color.Lerp(Color.magenta, Color.black, 0.25f)
    };

    private ListView _listView;

    public void CreateGUI()
    {
        _listView = new ListView();
        _listView.makeItem = () =>
        {
            var root = new VisualElement
            {
                style =
                {
                    height = 20,
                    flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row)
                }
            };

            // アイコン
            var icon = new VisualElement
            {
                name = "icon",
                style =
                {
                    width = 16,
                    height = 16,
                    marginBottom = 2,
                    marginLeft = 2,
                    marginRight = 2,
                    marginTop = 2
                }
            };
            root.Add(icon);

            // ラベル
            var label = new Label
            {
                name = "label",
                style =
                {
                    unityTextAlign = TextAnchor.MiddleCenter
                }
            };
            root.Add(label);

            return root;
        };
        _listView.bindItem = (element, i) =>
        {
            var color = (Color)_listView.itemsSource[i];

            // アイコンを設定
            var icon = element.Q("icon");
            icon.style.backgroundColor = color;

            // ラベルを設定
            var label = element.Q<Label>("label");
            label.text = ColorToString(color);
        };
        _listView.selectionType = SelectionType.Single;
        _listView.itemsSource = _sourceColors;

        _listView.RegisterCallback<ValidateCommandEvent>(OnValidateCommand);
        _listView.RegisterCallback<ExecuteCommandEvent>(OnExecuteCommand);

        rootVisualElement.Add(_listView);
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }

    // コマンドが実行可能かどうかを判定する
    // 当該コマンドが実行可能だったらStopPropagationを呼ぶ
    private void OnValidateCommand(ValidateCommandEvent evt)
    {
        switch (evt.commandName)
        {
            case "Copy" when _listView.selectedIndices.Any():
            {
                Debug.Log("stop");
                evt.StopPropagation();
                break;
            }
            case "Paste" when IsColorString(EditorGUIUtility.systemCopyBuffer):
            {
                evt.StopPropagation();
                break;
            }
        }
    }

    // コマンドを実行する
    // コマンドが実行されたらStopPropagationを呼ぶprivate void OnExecuteCommand(ExecuteCommandEvent evt)
    {
        switch (evt.commandName)
        {
            case "Copy":
            {
                evt.StopPropagation();
                var selectedColor = (Color)_listView.itemsSource[_listView.selectedIndex];
                EditorGUIUtility.systemCopyBuffer = ColorToString(selectedColor);
                break;
            }
            case "Paste":
            {
                evt.StopPropagation();
                var color = StringToColor(EditorGUIUtility.systemCopyBuffer);
                _listView.itemsSource[_listView.selectedIndex] = color;
                _listView.RefreshItem(_listView.selectedIndex);
                break;
            }
        }
    }

    private static string ColorToString(Color color)
    {
        return $"#{ColorUtility.ToHtmlStringRGB(color)}";
    }

    private static Color StringToColor(string colorString)
    {
        if (!ColorUtility.TryParseHtmlString(colorString, out var color))
            throw new Exception($"Invalid color string: {colorString}");

        return color;
    }

    private static bool IsColorString(string colorString)
    {
        return ColorUtility.TryParseHtmlString(colorString, out _);
    }
}

説明はコメントに書いた通りです。

コマンドが発行されるとまず、ValidateCommandEventが送られます。
このコマンドが実行可能だったら、StopPropagationを呼んで伝播を止めます。

ValidateCommandEventの伝播を止めた場合には、その後にExecuteCommandEventが送られます。
ExecuteCommandEventにはコマンドの実行処理を書いて、StopPropagationしてイベントの伝播を止めます。

動作確認

Example > Windowからウィンドウを開くと以下の結果が得られます。
いずれかの行を選択してコピーし他の行にペーストすると、行の色が貼り付けられることを確認できます。

動作確認

対応しているコマンド

マニュアルによるとコマンドには以下の種類があるようです。(詳細は調べてません)

  • Copy
  • Cut
  • Paste
  • Delete
  • SoftDelete
  • Duplicate
  • FrameSelected
  • FrameSelectedWithLock
  • SelectAll
  • Find
  • FocusProjectWindow

参考

docs.unity3d.com