UnitySearchで困ったところ

はじめに

Unity Search にプロジェクトで使用するデータを検索する機能を追加しようとした際に,
いくつか実装に手間取ったところがあったのでまとめました。
docs.unity3d.com

環境

Unity : 2022.3.10f1

困ったところ

filterId を検索フィールドに入力した時にのみ動作してほしい

愚直に SearchProvider を実装すると SearchWindow を開いた際,
検索フィールドに何も入力していないのに該当する要素が全て表示されてしまいます。

filterId を検索フィールドに入力した時にのみ
その filterId が有効になるようにするには
SearchProvider.isExplicitProvidertrue を設定します。

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        isExplicitProvider = true,
        filterId = "s:",
        fetchItems = FetchItems,
    };
}

private static IEnumerable<SearchItem> FetchItems(SearchContext context, List<SearchItem> items, SearchProvider provider)
{
    return AssetDatabase.GetAllAssetPaths().Select(path => provider.CreateItem(path));
}

以下のように filterId の s: を入力した時のみ要素が表示されるようになります。

Table View のカラムを追加したい

Table View とは Search Window の右下の格子状のアイコンを押した時の状態のことを指します。
この状態では Tree View のように要素ごとにカラムが表示され, より多くの情報を確認できます。

Table View では SearchWindow 右上の + ボタンから
カラムを追加することができます。

Table View で利用可能なカラムを追加するには
SearchProvider.fetchColumn
IEnumerable 型のカラム設定を返す関数を登録することで,
コンテキストメニューに要素を追加することができます。

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        filterId = "s:",
        fetchItems = FetchItems,
    };
}

private static IEnumerable<SearchItem> FetchItems(SearchContext context, List<SearchItem> items, SearchProvider provider)
{
    return AssetDatabase.GetAllAssetPaths().Select(path => provider.CreateItem(path));
}

private static IEnumerable<SearchColumn> FetchColumns(SearchContext context, IEnumerable<SearchItem> items)
{
    yield return new SearchColumn("path/sample column", "sample selector", "search column provider") { width = 200 };
}

カラムに表示するデータの抽出や描画の処理は後述の SearchColumnProviderAttribute を使用して定義します。

カラムの表示を変更したい

SearchColumnProviderAttribute を使用することで,
カラムの表示方法を定義するフォーマットを追加することができます。
docs.unity3d.com

このフォーマットは特定の SearchProvider に依存せず,
どの SearchProvider でも使用できるようになるようです。

なので, 表示内容を保有するデータは SearchProvider 毎に専用のデータ型を定義するより,
Dictionary<string, object> のようなものを使用するのが良いかもしれません。

Attribute の引数に provider を設定しますが, これは SearchProvider の id ではなく,
SearchColumnProvider の id となる名前を設定します。

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        filterId = "s:",
        fetchItems = FetchItems,
        fetchColumns = FetchColumns,
    };
}

private static IEnumerable<SearchItem> FetchItems(SearchContext context, List<SearchItem> items, SearchProvider provider)
{
    foreach (var path in AssetDatabase.GetAllAssetPaths())
    {
        var data = new Dictionary<string, object>() { { "sample selector", path } };
        yield return provider.CreateItem(path, null, null, null, data: data);
    }
}

private static IEnumerable<SearchColumn> FetchColumns(SearchContext context, IEnumerable<SearchItem> items)
{
    yield return new SearchColumn("path/sample column", "sample selector", "search column provider") { width = 200 };
}

// 
[SearchColumnProvider("search column provider")]
private static void SampleSearchColumnProvider(SearchColumn searchColumn)
{
    searchColumn.getter = args => (args.item.data as Dictionary<string, object>)[args.column.selector];
    searchColumn.drawer = args =>
    {
        GUI.Button(args.rect, args.value.ToString());
        return args.value;
    };
}

要素を右クリックした時のコンテキストメニューを追加したい

SearchActionsProviderAttribute で特定の SearchProvider にコンテキストメニューにアクションを追加できます。

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        filterId = "s:",
        fetchItems = FetchItems,
        fetchColumns = FetchColumns,
    };
}

// 略

[SearchActionsProvider]
private static IEnumerable<SearchAction> LogAction()
{
    // Provider 毎に使用可能な action を設定する.
    yield return new SearchAction("sample", "Log")
    {
        handler = item => Debug.Log((item.data as Dictionary<string, object>)["sample selector"]),
    };
}

通信などで遅延して要素を表示したい

一覧表示するデータを通信して取得したい時など,
遅延して要素を SearchWindow に表示したいことがあります。

正しいやり方か不明ですが, FetchItems の中で非同期処理をトリガーし,
取得が完了したタイミングで SearchService.RefreshWindows() を実行してあげると,
再度 FetchItems が走り, 表示を更新することができます。

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        filterId = "s:",
        fetchItems = FetchItems,
        fetchColumns = FetchColumns,
    };
}

private static List<SearchItem> m_CachedItems;

private static IEnumerable<SearchItem> FetchItems(SearchContext context, List<SearchItem> items, SearchProvider provider)
{
    if (m_CachedItems != null)
    {
        foreach (var item in m_CachedItems)
        {
            yield return item;
        }
        
        yield break;
    }

    m_CachedItems = new();
    GetAllAssetPathsAsync().ContinueWith(result =>
    {
        foreach (var path in AssetDatabase.GetAllAssetPaths())
        {
            var data = new Dictionary<string, object>() { { "sample selector", path } };
            m_CachedItems.Add(provider.CreateItem(path, null, null, null, data: data));
        }

        SearchService.RefreshWindows();
    }).Forget();

    yield break;
}

private static async UniTask<List<string>> GetAllAssetPathsAsync()
{
    await UniTask.WaitForSeconds(5);
    return AssetDatabase.GetAllAssetPaths().ToList();
}

検索処理を実装したい

QueryEngine を使用することで, クエリ演算子を用いた検索機能を実装することが可能です。
また, スペース区切りで or 検索になるなど, 基本的なフィルタ機能を備えているので, 大体これで事足りそうです。 docs.unity3d.com

[SearchItemProvider]
private static SearchProvider CreateProvider()
{
    return new SearchProvider("sample", "Sample Search Provider")
    {
        filterId = "s:",
        fetchItems = FetchItems,
        fetchColumns = FetchColumns,
    };
}

private static IEnumerable<SearchItem> FetchItems(SearchContext context, List<SearchItem> items, SearchProvider provider)
{
    var searchItems = AssetDatabase.GetAllAssetPaths()
        .Select(path =>
        {
            var data = new Dictionary<string, object>() { { "sample selector", path } };
            return provider.CreateItem(path, null, null, null, data: data);
        }).ToList();

    var queryEngine = new QueryEngine<SearchItem>();
     // クエリのトークンと使用するデータを追加. sample:○○ でフィルタできるようになる.
    queryEngine.AddFilter("sample", x => (string)(x.data as Dictionary<string, object>)["sample selector"]);
    // 検索に使用するテキストを指定. 今回は CreateItem で path を id に使用しているので, id を指定.
    queryEngine.SetSearchDataCallback(x => new[] { x.id });
    var query = queryEngine.ParseQuery(context.searchQuery);
    foreach (var item in query.Apply(searchItems))
    {
        yield return item;
    }
}