-
Notifications
You must be signed in to change notification settings - Fork 0
Update/main window #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a5e3293
1de4433
8e8f94f
ad3a847
05965a7
1a6ce98
8da961d
6e4b7aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,6 @@ namespace com.IvanMurzak.Unity.MCP.Editor.UI | |
| public class ConfigurationElements : IDisposable | ||
| { | ||
| public VisualElement Root { get; } | ||
| public VisualElement StatusCircle { get; } | ||
| public Label StatusText { get; } | ||
| public Button BtnConfigure { get; } | ||
|
|
||
|
|
@@ -37,17 +36,33 @@ public ConfigurationElements(AiAgentConfig config, TransportMethod transportMode | |
| _transportMode = transportMode; | ||
|
|
||
| Root = new UITemplate<VisualElement>("Editor/UI/uxml/agents/elements/TemplateConfigureStatus.uxml").Value; | ||
| StatusCircle = Root.Q<VisualElement>("configureStatusCircle") ?? throw new NullReferenceException("VisualElement 'configureStatusCircle' not found in UI."); | ||
| StatusText = Root.Q<Label>("configureStatusText") ?? throw new NullReferenceException("Label 'configureStatusText' not found in UI."); | ||
| BtnConfigure = Root.Q<Button>("btnConfigure") ?? throw new NullReferenceException("Button 'btnConfigure' not found in UI."); | ||
|
|
||
| var pathLabel = Root.Q<Label>("labelConfigPath"); | ||
| if (pathLabel != null) | ||
| { | ||
| pathLabel.text = _config.ConfigPath; | ||
| pathLabel.tooltip = _config.ConfigPath; | ||
| } | ||
|
|
||
| UpdateStatus(); | ||
|
|
||
| _clickCallback = new EventCallback<ClickEvent>(evt => | ||
| { | ||
| var result = _config.Configure(); | ||
| UpdateStatus(result); | ||
| onConfigured.OnNext(result); | ||
| var isCurrentlyConfigured = _config.IsConfigured(); | ||
| if (isCurrentlyConfigured) | ||
| { | ||
| _config.Unconfigure(); | ||
| UpdateStatus(false); | ||
| onConfigured.OnNext(false); | ||
| } | ||
| else | ||
| { | ||
| var result = _config.Configure(); | ||
| UpdateStatus(result); | ||
| onConfigured.OnNext(result); | ||
| } | ||
|
Comment on lines
+53
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check The 🛠️ Suggested fix if (isCurrentlyConfigured)
{
- _config.Unconfigure();
- UpdateStatus(false);
- onConfigured.OnNext(false);
+ var unconfigured = _config.Unconfigure();
+ if (unconfigured)
+ {
+ UpdateStatus(false);
+ onConfigured.OnNext(false);
+ }
+ // Optionally: else show error feedback
}🤖 Prompt for AI Agents |
||
| }); | ||
| BtnConfigure.RegisterCallback(_clickCallback); | ||
| } | ||
|
|
@@ -64,16 +79,9 @@ public void UpdateStatus(bool? isConfigured = null) | |
|
|
||
| StatusText.text = isConfiguredValue ? $"Configured ({transportText})" : "Not Configured"; | ||
|
|
||
| StatusCircle.RemoveFromClassList(MainWindowEditor.USS_Connected); | ||
| StatusCircle.RemoveFromClassList(MainWindowEditor.USS_Connecting); | ||
| StatusCircle.RemoveFromClassList(MainWindowEditor.USS_Disconnected); | ||
| StatusCircle.AddToClassList(isConfiguredValue | ||
| ? MainWindowEditor.USS_Connected | ||
| : MainWindowEditor.USS_Disconnected); | ||
|
|
||
| BtnConfigure.text = isConfiguredValue ? "Reconfigure" : "Configure"; | ||
| BtnConfigure.text = isConfiguredValue ? "Remove" : "Configure"; | ||
| BtnConfigure.EnableInClassList("btn-primary", !isConfiguredValue); | ||
| BtnConfigure.EnableInClassList("btn-secondary", isConfiguredValue); | ||
| BtnConfigure.EnableInClassList("btn-alert", isConfiguredValue); | ||
| } | ||
|
|
||
| public void Dispose() | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| /* | ||
| ┌──────────────────────────────────────────────────────────────────┐ | ||
| │ 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 com.IvanMurzak.Unity.MCP.Editor.Utils; | ||
| using UnityEngine.UIElements; | ||
|
|
||
| namespace com.IvanMurzak.Unity.MCP.Editor.UI.Controls | ||
| { | ||
| /// <summary> | ||
| /// A segmented control with N segments and a sliding highlight. | ||
| /// Implements <see cref="INotifyValueChanged{T}"/> for UI Toolkit integration. | ||
| /// All visual styling is defined in <c>_segmented-control.uss</c>. | ||
| /// </summary> | ||
| public class SegmentedControl : VisualElement, INotifyValueChanged<int> | ||
| { | ||
| private static readonly string[] UssPaths = | ||
| EditorAssetLoader.GetEditorAssetPaths("Editor/UI/uss/common/_segmented-control.uss"); | ||
|
|
||
| public new class UxmlFactory : UxmlFactory<SegmentedControl, UxmlTraits> { } | ||
|
|
||
| public new class UxmlTraits : VisualElement.UxmlTraits | ||
| { | ||
| private readonly UxmlStringAttributeDescription _choices = new() { name = "choices", defaultValue = "" }; | ||
| private readonly UxmlIntAttributeDescription _defaultIndex = new() { name = "default-index", defaultValue = 0 }; | ||
|
|
||
| public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) | ||
| { | ||
| base.Init(ve, bag, cc); | ||
| var control = (SegmentedControl)ve; | ||
| var choicesValue = _choices.GetValueFromBag(bag, cc); | ||
| var defaultIndex = _defaultIndex.GetValueFromBag(bag, cc); | ||
|
|
||
| if (!string.IsNullOrEmpty(choicesValue)) | ||
| { | ||
| var labels = choicesValue.Split(','); | ||
| for (var i = 0; i < labels.Length; i++) | ||
| labels[i] = labels[i].Trim(); | ||
| control.BuildSegments(labels); | ||
| control.SetValueWithoutNotify(defaultIndex); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private readonly VisualElement _track; | ||
| private readonly VisualElement _highlight; | ||
| private readonly List<Label> _segments = new(); | ||
| private int _selectedIndex; | ||
| private bool _transitionReady; | ||
|
|
||
| public int SelectedIndex | ||
| { | ||
| get => _selectedIndex; | ||
| set => SetValue(value); | ||
| } | ||
|
|
||
| int INotifyValueChanged<int>.value | ||
| { | ||
| get => _selectedIndex; | ||
| set => SetValue(value); | ||
| } | ||
|
|
||
| public SegmentedControl() : this(Array.Empty<string>()) { } | ||
|
|
||
| public SegmentedControl(params string[] labels) | ||
| { | ||
| AddToClassList("segmented-control"); | ||
|
|
||
| var uss = EditorAssetLoader.LoadAssetAtPath<StyleSheet>(UssPaths); | ||
| if (uss != null) | ||
| styleSheets.Add(uss); | ||
|
|
||
| _track = new VisualElement(); | ||
| _track.AddToClassList("segmented-control__track"); | ||
| Add(_track); | ||
|
|
||
| _highlight = new VisualElement(); | ||
| _highlight.AddToClassList("segmented-control__highlight"); | ||
| _highlight.AddToClassList("segmented-control__highlight--no-transition"); | ||
| _track.Add(_highlight); | ||
|
|
||
| if (labels.Length > 0) | ||
| BuildSegments(labels); | ||
| } | ||
|
|
||
| private void BuildSegments(string[] labels) | ||
| { | ||
| // Remove old segments (keep highlight at index 0) | ||
| foreach (var seg in _segments) | ||
| _track.Remove(seg); | ||
| _segments.Clear(); | ||
|
|
||
| for (var i = 0; i < labels.Length; i++) | ||
| { | ||
| var label = new Label(labels[i]); | ||
| label.AddToClassList("segmented-control__segment"); | ||
| var index = i; | ||
| label.RegisterCallback<ClickEvent>(_ => SelectedIndex = index); | ||
| label.RegisterCallback<GeometryChangedEvent>(_ => SnapHighlightToSegment()); | ||
| _track.Add(label); | ||
| _segments.Add(label); | ||
| } | ||
|
|
||
| _transitionReady = false; | ||
| UpdateHighlight(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets per-segment tooltips. Segment count must match. | ||
| /// </summary> | ||
| public void SetTooltips(params string[] tooltips) | ||
| { | ||
| for (var i = 0; i < tooltips.Length && i < _segments.Count; i++) | ||
| _segments[i].tooltip = tooltips[i]; | ||
| } | ||
|
|
||
| public void SetValueWithoutNotify(int newValue) | ||
| { | ||
| if (newValue < 0 || (_segments.Count > 0 && newValue >= _segments.Count)) | ||
| return; | ||
|
|
||
| _selectedIndex = newValue; | ||
| UpdateHighlight(); | ||
| } | ||
|
|
||
| private void SetValue(int newValue) | ||
| { | ||
| if (newValue < 0 || (_segments.Count > 0 && newValue >= _segments.Count)) | ||
| return; | ||
| if (newValue == _selectedIndex) | ||
| return; | ||
|
|
||
| using var evt = ChangeEvent<int>.GetPooled(_selectedIndex, newValue); | ||
| evt.target = this; | ||
| _selectedIndex = newValue; | ||
| UpdateHighlight(); | ||
| SendEvent(evt); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Positions the highlight to match the selected segment's actual layout. | ||
| /// Called on geometry changes and selection changes. | ||
| /// </summary> | ||
| private void SnapHighlightToSegment() | ||
| { | ||
| if (_segments.Count == 0 || _selectedIndex < 0 || _selectedIndex >= _segments.Count) | ||
| return; | ||
|
|
||
| var segment = _segments[_selectedIndex]; | ||
| var x = segment.layout.x; | ||
| var w = segment.layout.width; | ||
|
|
||
| // layout is zero before first layout pass | ||
| if (float.IsNaN(w) || w <= 0) | ||
| return; | ||
|
|
||
| _highlight.style.left = x; | ||
| _highlight.style.width = w; | ||
| } | ||
|
|
||
| private void UpdateHighlight() | ||
| { | ||
| if (_segments.Count == 0) | ||
| return; | ||
|
|
||
| if (!_transitionReady) | ||
| { | ||
| // Start with transitions suppressed; enable after first layout | ||
| SnapHighlightToSegment(); | ||
| schedule.Execute(() => | ||
| { | ||
| _transitionReady = true; | ||
| _highlight.RemoveFromClassList("segmented-control__highlight--no-transition"); | ||
| }); | ||
| } | ||
| else | ||
| { | ||
| SnapHighlightToSegment(); | ||
| } | ||
|
|
||
| for (var i = 0; i < _segments.Count; i++) | ||
| _segments[i].EnableInClassList("segmented-control__segment--selected", i == _selectedIndex); | ||
| } | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The remove branch assumes unconfiguration always succeeds and force-updates the UI/event state to "not configured". Since
Unconfigure()can returnfalse(e.g., file missing or write failure), this can desynchronize UI state from actual config state and emit incorrect status to subscribers. Re-read the actual configured state after attempting removal and publish that. [logic error]Severity Level: Major⚠️
Steps of Reproduction ✅
Prompt for AI Agent 🤖