/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.Unity.MCP.Editor.Utils;
using Microsoft.Extensions.Logging;
using R3;
using UnityEditor;
using UnityEngine.UIElements;
namespace com.IvanMurzak.Unity.MCP.Editor.UI
{
///
/// Filter type for MCP list windows (Tools, Prompts, Resources).
///
public enum McpFilterType
{
All,
Enabled,
Disabled
}
///
/// Base class for MCP list windows providing common ListView functionality.
///
/// The view model type for list items.
public abstract class McpListWindowBase : McpWindowBase
where TViewModel : class, IMcpItemViewModel
{
private const string FilterStatsFormat = "Filtered: {0}, Total: {1}";
protected abstract string[] ItemUxmlPaths { get; }
protected abstract string MissingTemplateMessage { get; }
protected readonly CompositeDisposable _disposables = new();
private IDisposable? _innerSubscription;
protected VisualTreeAsset? itemTemplate;
protected List allItems = new();
protected ListView? listView;
protected Label? emptyListLabel;
protected TextField? filterField;
protected DropdownField? typeDropdown;
protected Label? filterStatsLabel;
public override void CreateGUI()
{
rootVisualElement.Clear();
_disposables.Clear();
InitializePlugin();
var visualTree = EditorAssetLoader.LoadAssetAtPath(WindowUxmlPaths, Logger);
if (visualTree == null)
return;
visualTree.CloneTree(rootVisualElement);
ApplyStyleSheets(rootVisualElement);
itemTemplate = EditorAssetLoader.LoadAssetAtPath(ItemUxmlPaths, Logger);
InitializeFilters(rootVisualElement);
RefreshItems();
PopulateList();
SubscribeToUpdates();
}
protected virtual void OnDisable()
{
_innerSubscription?.Dispose();
_innerSubscription = null;
_disposables.Clear();
}
protected virtual Observable? GetOnUpdatedObservable(IMcpPlugin plugin) => null;
private void SubscribeToUpdates()
{
UnityMcpPluginEditor.PluginProperty
.Subscribe(plugin =>
{
_innerSubscription?.Dispose();
_innerSubscription = null;
if (plugin == null)
return;
_innerSubscription = GetOnUpdatedObservable(plugin)?
.ObserveOnCurrentSynchronizationContext()
.Subscribe(_ =>
{
RefreshItems();
PopulateList();
});
}).AddTo(_disposables);
}
protected void InitializePlugin()
{
UnityMcpPluginEditor.InitSingletonIfNeeded();
UnityMcpPluginEditor.Instance.BuildMcpPluginIfNeeded();
UnityMcpPluginEditor.Instance.AddUnityLogCollectorIfNeeded(() => new BufferedFileLogStorage());
}
protected void InitializeFilters(VisualElement root)
{
filterField = root.Q("filter-textfield");
if (filterField != null)
filterField.RegisterValueChangedCallback(evt => PopulateList());
typeDropdown = root.Q("type-dropdown");
if (typeDropdown != null)
{
typeDropdown.choices = Enum.GetNames(typeof(McpFilterType)).ToList();
typeDropdown.index = (int)McpFilterType.All;
typeDropdown.RegisterValueChangedCallback(evt => PopulateList());
}
filterStatsLabel = root.Q