UTKToolBar 완료

This commit is contained in:
logonkhi
2026-02-20 19:17:36 +09:00
parent ad10e24d13
commit b64c3e10bc
26 changed files with 882 additions and 153 deletions

View File

@@ -10,17 +10,13 @@ namespace UVC.UIToolkit
{
/// <summary>
/// VisualElement에 툴팁을 설정합니다.
/// UTKTooltipManager가 초기화되어 있어야 합니다.
/// </summary>
/// <param name="element">대상 요소</param>
/// <param name="tooltip">툴팁 텍스트 또는 다국어 키</param>
/// <returns>체이닝을 위한 원본 요소</returns>
public static T SetTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.AttachTooltip(element, tooltip);
}
UTKTooltipManager.Instance.AttachTooltip(element, tooltip);
return element;
}
@@ -31,10 +27,7 @@ namespace UVC.UIToolkit
/// <returns>체이닝을 위한 원본 요소</returns>
public static T ClearTooltip<T>(this T element) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.DetachTooltip(element);
}
UTKTooltipManager.Instance.DetachTooltip(element);
return element;
}
@@ -46,10 +39,7 @@ namespace UVC.UIToolkit
/// <returns>체이닝을 위한 원본 요소</returns>
public static T UpdateTooltip<T>(this T element, string tooltip) where T : VisualElement
{
if (UTKTooltipManager.Instance.IsInitialized)
{
UTKTooltipManager.Instance.UpdateTooltip(element, tooltip);
}
UTKTooltipManager.Instance.UpdateTooltip(element, tooltip);
return element;
}
}

View File

@@ -12,6 +12,7 @@ namespace UVC.UIToolkit
/// <summary>
/// UIToolkit 기반 툴팁 매니저.
/// VisualElement에 마우스 오버 시 툴팁을 표시하는 싱글톤 관리자입니다.
/// panel.visualTree를 사용하여 모든 UI 위에 툴팁을 표시합니다.
/// </summary>
/// <remarks>
/// <para><b>Tooltip(툴팁)이란?</b></para>
@@ -25,7 +26,7 @@ namespace UVC.UIToolkit
/// <para>
/// UTKTooltipManager는 싱글톤으로 구현되어 있습니다.
/// <c>UTKTooltipManager.Instance</c>로 접근하며, 앱 전체에서 하나의 툴팁 UI를 공유합니다.
/// 사용 전에 반드시 <c>Initialize(root)</c>를 호출해야 합니다.
/// panel.visualTree를 사용하므로 별도 Initialize 호출이 필요 없습니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
@@ -38,7 +39,6 @@ namespace UVC.UIToolkit
///
/// <para><b>주요 메서드:</b></para>
/// <list type="bullet">
/// <item><description><c>Initialize(root)</c> - 초기화 (루트 요소 지정)</description></item>
/// <item><description><c>AttachTooltip(element, text)</c> - 요소에 툴팁 연결</description></item>
/// <item><description><c>DetachTooltip(element)</c> - 툴팁 제거</description></item>
/// <item><description><c>Show(text, position)</c> - 즉시 표시</description></item>
@@ -56,20 +56,17 @@ namespace UVC.UIToolkit
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// // 1. 초기화 (앱 시작 시 한 번)
/// UTKTooltipManager.Instance.Initialize(rootVisualElement);
///
/// // 2. 버튼에 툴팁 연결
/// // 1. 버튼에 툴팁 연결 (Initialize 불필요)
/// var saveButton = new UTKButton("", UTKMaterialIcons.Save);
/// UTKTooltipManager.Instance.AttachTooltip(saveButton, "저장 (Ctrl+S)");
///
/// // 3. 다국어 키로 툴팁 연결
/// // 2. 다국어 키로 툴팁 연결
/// UTKTooltipManager.Instance.AttachTooltip(settingsButton, "tooltip_settings");
///
/// // 4. 툴팁 업데이트
/// // 3. 툴팁 업데이트
/// UTKTooltipManager.Instance.UpdateTooltip(button, "새로운 설명");
///
/// // 5. 툴팁 제거
/// // 4. 툴팁 제거
/// UTKTooltipManager.Instance.DetachTooltip(button);
/// </code>
/// </example>
@@ -90,12 +87,11 @@ namespace UVC.UIToolkit
#endregion
#region Fields
private VisualElement? _root;
private VisualElement? _tooltipContainer;
private Label? _tooltipLabel;
private bool _isInitialized;
private bool _isVisible;
private bool _disposed;
private StyleSheet? _loadedUss;
private CancellationTokenSource? _showDelayCts;
private readonly Dictionary<VisualElement, string> _tooltipRegistry = new();
@@ -105,24 +101,19 @@ namespace UVC.UIToolkit
#endregion
#region Properties
public bool IsInitialized => _isInitialized;
public bool IsVisible => _isVisible;
#endregion
#region Initialization
/// <summary>
/// 툴팁 매니저를 초기화합니다.
/// 툴팁 UI를 생성합니다 (아직 visual tree에 추가하지 않음).
/// </summary>
/// <param name="root">VisualElement 트리의 루트</param>
public void Initialize(VisualElement root)
private void EnsureTooltipUI()
{
if (_isInitialized)
{
Debug.LogWarning("[UTKTooltipManager] Already initialized.");
return;
}
if (_tooltipContainer != null) return;
_root = root;
// USS 로드
_loadedUss = Resources.Load<StyleSheet>(USS_PATH);
// UXML 로드 시도
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
@@ -143,13 +134,11 @@ namespace UVC.UIToolkit
_tooltipContainer.style.position = Position.Absolute;
_tooltipContainer.style.display = DisplayStyle.None;
_tooltipContainer.pickingMode = PickingMode.Ignore;
_root.Add(_tooltipContainer);
}
// 테마 변경 이벤트 구독
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
_isInitialized = true;
}
/// <summary>
@@ -174,16 +163,6 @@ namespace UVC.UIToolkit
pickingMode = PickingMode.Ignore
};
// 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
// USS 스타일시트 로드
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
{
_tooltipContainer.styleSheets.Add(uss);
}
// USS 클래스로 스타일 적용
_tooltipContainer.AddToClassList("utk-tooltip-container");
@@ -196,6 +175,33 @@ namespace UVC.UIToolkit
_tooltipContainer.Add(_tooltipLabel);
}
/// <summary>
/// 툴팁 컨테이너를 대상 요소의 panel.visualTree에 추가합니다.
/// </summary>
/// <param name="element">대상 요소 (panel 접근용)</param>
private void AttachToPanel(VisualElement element)
{
if (_tooltipContainer == null || element.panel == null) return;
var visualTree = element.panel.visualTree;
// 이미 해당 visualTree에 추가되어 있으면 스킵
if (_tooltipContainer.parent == visualTree) return;
// 다른 곳에 붙어 있으면 제거
_tooltipContainer.RemoveFromHierarchy();
// panel.visualTree에 추가
visualTree.Add(_tooltipContainer);
// 테마/USS 재적용
UTKThemeManager.Instance.ApplyThemeToElement(_tooltipContainer);
if (_loadedUss != null)
{
_tooltipContainer.styleSheets.Add(_loadedUss);
}
}
#endregion
#region Public Methods
@@ -203,10 +209,10 @@ namespace UVC.UIToolkit
/// 툴팁을 즉시 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
/// <param name="position">월드 좌표</param>
public void Show(string text, Vector2 position)
{
if (!_isInitialized || _tooltipContainer == null || _tooltipLabel == null)
if (_tooltipContainer == null || _tooltipLabel == null)
return;
// 다국어 처리
@@ -230,7 +236,7 @@ namespace UVC.UIToolkit
/// 지연 후 툴팁을 표시합니다.
/// </summary>
/// <param name="text">표시할 텍스트</param>
/// <param name="position">화면 좌표</param>
/// <param name="position">월드 좌표</param>
/// <param name="delayMs">지연 시간 (밀리초)</param>
public async UniTaskVoid ShowDelayed(string text, Vector2 position, int delayMs = SHOW_DELAY_MS)
{
@@ -275,21 +281,22 @@ namespace UVC.UIToolkit
// 기존 등록 제거
DetachTooltip(element);
// 툴팁 UI 생성 보장
EnsureTooltipUI();
_tooltipRegistry[element] = tooltip;
// 이벤트 콜백 생성 및 등록
// 참고: evt.position은 로컬 좌표이므로, 패널 기준 좌표로 변환 필요
EventCallback<PointerEnterEvent> enterCallback = evt =>
{
if (_tooltipRegistry.TryGetValue(element, out var text))
{
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
ShowDelayed(text, rootPosition).Forget();
// panel.visualTree에 툴팁 컨테이너 추가
AttachToPanel(element);
// worldBound 기준 좌표 사용
var worldPos = element.LocalToWorld(evt.localPosition);
ShowDelayed(text, worldPos).Forget();
}
};
@@ -299,13 +306,9 @@ namespace UVC.UIToolkit
{
if (_isVisible)
{
// 로컬 좌표를 root 좌표로 변환
var rootPosition = element.LocalToWorld(evt.localPosition);
if (_root != null)
{
rootPosition = _root.WorldToLocal(rootPosition);
}
AdjustPosition(rootPosition);
// worldBound 기준 좌표 사용
var worldPos = element.LocalToWorld(evt.localPosition);
AdjustPosition(worldPos);
}
};
@@ -406,37 +409,39 @@ namespace UVC.UIToolkit
}
/// <summary>
/// 화면 경계 내에서 위치 조정
/// 화면 경계 내에서 위치 조정 (월드 좌표 기준)
/// </summary>
private void AdjustPosition(Vector2 mousePosition)
private void AdjustPosition(Vector2 worldPosition)
{
if (_tooltipContainer == null || _root == null)
if (_tooltipContainer == null || _tooltipContainer.panel == null)
return;
var panelRoot = _tooltipContainer.panel.visualTree;
var tooltipSize = new Vector2(
_tooltipContainer.resolvedStyle.width,
_tooltipContainer.resolvedStyle.height
);
var rootSize = new Vector2(
_root.resolvedStyle.width,
_root.resolvedStyle.height
var panelSize = new Vector2(
panelRoot.resolvedStyle.width,
panelRoot.resolvedStyle.height
);
// 기본 위치: 마우스 오른쪽 아래
float x = mousePosition.x + POSITION_OFFSET;
float y = mousePosition.y + POSITION_OFFSET;
float x = worldPosition.x + POSITION_OFFSET;
float y = worldPosition.y + POSITION_OFFSET;
// 오른쪽 경계 체크
if (x + tooltipSize.x > rootSize.x)
if (x + tooltipSize.x > panelSize.x)
{
x = mousePosition.x - tooltipSize.x - POSITION_OFFSET;
x = worldPosition.x - tooltipSize.x - POSITION_OFFSET;
}
// 아래쪽 경계 체크
if (y + tooltipSize.y > rootSize.y)
if (y + tooltipSize.y > panelSize.y)
{
y = mousePosition.y - tooltipSize.y - POSITION_OFFSET;
y = worldPosition.y - tooltipSize.y - POSITION_OFFSET;
}
// 왼쪽/위쪽 경계 체크
@@ -476,9 +481,7 @@ namespace UVC.UIToolkit
_tooltipContainer?.RemoveFromHierarchy();
_tooltipContainer = null;
_tooltipLabel = null;
_root = null;
_isInitialized = false;
_isVisible = false;
_instance = null;
}

View File

@@ -4,6 +4,7 @@ using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Extention;
namespace UVC.UIToolkit
{
@@ -233,6 +234,7 @@ namespace UVC.UIToolkit
if (_label != null)
{
_label.selection.isSelectable = value;
_label.pickingMode = value ? PickingMode.Position : PickingMode.Ignore;
}
}
}
@@ -456,7 +458,7 @@ namespace UVC.UIToolkit
{
AddToClassList("utk-label");
_label = new Label { name = "label" };
_label = new Label { name = "label", pickingMode = PickingMode.Ignore };
_label.AddToClassList("utk-label__text");
Add(_label);
@@ -621,14 +623,14 @@ namespace UVC.UIToolkit
// 아이콘과 텍스트 사이의 간격을 적용
if (_iconLabel != null)
{
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
_iconLabel.style.marginRight = _iconPosition == IconPosition.Left ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
_iconLabel.style.marginLeft = _iconPosition == IconPosition.Right ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
}
if (_imageIcon != null)
{
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? _gap : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? _gap : 0;
_imageIcon.style.marginRight = _iconPosition == IconPosition.Left ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
_imageIcon.style.marginLeft = _iconPosition == IconPosition.Right ? (_text.IsNullOrEmpty() ? 0 : _gap) : 0;
}
}
@@ -669,7 +671,7 @@ namespace UVC.UIToolkit
_iconLabel.style.display = DisplayStyle.Flex;
UTKMaterialIcons.ApplyIconStyle(_iconLabel, fontSize ?? GetEffectiveIconSize());
}
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
@@ -693,6 +695,7 @@ namespace UVC.UIToolkit
await UTKMaterialIcons.ApplyIconStyleAsync(_iconLabel, ct, fontSize ?? GetEffectiveIconSize());
}
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();
@@ -816,6 +819,7 @@ namespace UVC.UIToolkit
_imageIcon.style.backgroundImage = new StyleBackground(texture);
_imageIcon.style.display = DisplayStyle.Flex;
if(_text.IsNullOrEmpty()) TextAlignment = TextAlign.Center; // 텍스트가 없는 경우 아이콘 중앙 정렬
EnableInClassList("utk-label--has-icon", true);
UpdateIconPosition();
UpdateGap();

View File

@@ -874,7 +874,8 @@ namespace UVC.UIToolkit
title = new UTKLabel();
title.AddToClassList("utk-property-group__title");
title.Size = UTKLabel.LabelSize.Label2;
title.IsBold = true;
// count = new UTKLabel();
// count.AddToClassList("utk-property-group__count");
@@ -887,8 +888,7 @@ namespace UVC.UIToolkit
// 데이터 바인딩
expandIcon.SetMaterialIcon(group.IsExpanded ? UTKMaterialIcons.ExpandMore : UTKMaterialIcons.ChevronRight, 16);
title.Text = group.GroupName;
title.Size = UTKLabel.LabelSize.Label1;
title.IsBold = true;
// count.Text = $"({group.ItemCount})";
// count.Variant = UTKLabel.LabelVariant.Secondary;

View File

@@ -90,11 +90,32 @@ namespace UVC.UIToolkit
_listView.selectionType = SelectionType.Single;
_listView.reorderable = true;
_listView.reorderMode = ListViewReorderMode.Animated;
_listView.selectionType = SelectionType.None; // 선택 비활성화 (체크박스 사용)
_listView.itemIndexChanged += OnItemIndexChanged;
// Unity 내장 reorderable-handle 숨김 (CSS 선택자가 매칭되지 않는 경우 대비)
_listView.RegisterCallback<AttachToPanelEvent>(_ =>
{
_listView.schedule.Execute(() => HideBuiltInHandles()).ExecuteLater(50);
});
Add(_listView);
}
/// <summary>
/// Unity ListView 내장 드래그 핸들을 숨깁니다.
/// 커스텀 드래그 핸들(DragIndicator 아이콘)을 사용하므로 내장 핸들은 불필요합니다.
/// </summary>
private void HideBuiltInHandles()
{
if (_listView == null) return;
_listView.Query(className: "unity-list-view__reorderable-handle").ForEach(el =>
{
el.style.display = DisplayStyle.None;
el.style.width = 0;
});
}
private void SubscribeToThemeChanges()
{
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
@@ -137,9 +158,9 @@ namespace UVC.UIToolkit
root.styleSheets.Add(_itemStyleSheet);
// 드래그 핸들 아이콘 설정
var handle = root.Q<Label>("drag-handle");
var handle = root.Q<UTKLabel>("drag-handle");
if (handle != null)
handle.text = UTKMaterialIcons.DragIndicator;
handle.SetMaterialIcon(UTKMaterialIcons.DragIndicator);
return root;
}
@@ -156,7 +177,7 @@ namespace UVC.UIToolkit
container.style.alignItems = Align.Center;
container.style.minHeight = ITEM_HEIGHT;
var handle = new Label(UTKMaterialIcons.DragIndicator);
var handle = new UTKLabel("", UTKMaterialIcons.DragIndicator);
handle.name = "drag-handle";
handle.AddToClassList("reordable-list-item__drag-handle");
container.Add(handle);
@@ -178,6 +199,9 @@ namespace UVC.UIToolkit
{
if (index < 0 || index >= _items.Count) return;
// ListView가 makeItem 반환 후 inline flex-grow:0을 강제하므로 bindItem에서 덮어씀
element.style.flexGrow = 1;
var data = _items[index];
// 요소 참조

View File

@@ -15,10 +15,10 @@ namespace UVC.UIToolkit
#region Fields
/// <summary>아이콘 요소 (Material Icon Label 또는 Image)</summary>
protected Label? _iconLabel;
protected UTKLabel? _iconLabel;
/// <summary>텍스트 라벨</summary>
protected Label? _textLabel;
protected UTKLabel? _textLabel;
/// <summary>루트 버튼 요소</summary>
protected VisualElement? _rootButton;
@@ -103,12 +103,13 @@ namespace UVC.UIToolkit
{
var root = asset.Instantiate();
_rootButton = root.Q<VisualElement>("button-root");
_iconLabel = root.Q<Label>("icon");
_textLabel = root.Q<Label>("label");
_iconLabel = root.Q<UTKLabel>("icon");
_textLabel = root.Q<UTKLabel>("label");
_textLabel.Size = UTKLabel.LabelSize.Caption; // UXML에서 기본 크기를 설정하므로 코드에서 다시 지정
// TemplateContainer가 아이콘 정렬을 방해하지 않도록 설정
root.style.flexGrow = 1;
root.style.alignItems = Align.Center;
root.style.alignItems = Align.Stretch;
root.style.justifyContent = Justify.Center;
Add(root);
@@ -129,12 +130,13 @@ namespace UVC.UIToolkit
_rootButton = new VisualElement();
_rootButton.AddToClassList("utk-toolbar-btn");
_iconLabel = new Label();
_iconLabel = new UTKLabel();
_iconLabel.AddToClassList("utk-toolbar-btn__icon");
_rootButton.Add(_iconLabel);
_textLabel = new Label();
_textLabel = new UTKLabel();
_textLabel.AddToClassList("utk-toolbar-btn__label");
_textLabel.Size = UTKLabel.LabelSize.Caption;
_rootButton.Add(_textLabel);
Add(_rootButton);
@@ -164,6 +166,7 @@ namespace UVC.UIToolkit
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
UpdateTooltip(_data.Tooltip);
}
/// <summary>
@@ -216,29 +219,15 @@ namespace UVC.UIToolkit
if (useMaterialIcon)
{
// Material Icon (폰트 기반)
// Material Icon (폰트 기반) - UTKLabel의 SetMaterialIcon 사용
_iconLabel.RemoveFromClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = iconPath;
_iconLabel.style.backgroundImage = StyleKeyword.None;
_iconLabel.SetMaterialIcon(iconPath);
}
else
{
// 이미지 아이콘
// 이미지 아이콘 - UTKLabel의 SetImageIcon 사용
_iconLabel.AddToClassList("utk-toolbar-btn__icon--image");
_iconLabel.text = "";
var sprite = Resources.Load<Sprite>(iconPath);
if (sprite != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(sprite);
}
else
{
var texture = Resources.Load<Texture2D>(iconPath);
if (texture != null)
{
_iconLabel.style.backgroundImage = new StyleBackground(texture);
}
}
_iconLabel.SetImageIcon(iconPath);
}
}
@@ -250,7 +239,26 @@ namespace UVC.UIToolkit
{
if (_textLabel != null)
{
_textLabel.text = text;
_textLabel.Text = text;
_textLabel.style.display = string.IsNullOrEmpty(text) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
/// <summary>
/// 툴팁을 업데이트합니다.
/// </summary>
/// <param name="tooltipText">툴팁 텍스트</param>
protected void UpdateTooltip(string? tooltipText)
{
if (_rootButton == null) return;
if (string.IsNullOrEmpty(tooltipText))
{
UTKTooltipManager.Instance.DetachTooltip(_rootButton);
}
else
{
UTKTooltipManager.Instance.UpdateTooltip(_rootButton, tooltipText);
}
}
@@ -282,6 +290,7 @@ namespace UVC.UIToolkit
UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
UpdateText(_data.Text);
UpdateEnabled(_data.IsEnabled);
UpdateTooltip(_data.Tooltip);
}
#endregion
@@ -354,6 +363,12 @@ namespace UVC.UIToolkit
// 데이터 바인딩 해제
UnbindData();
// 툴팁 해제
if (_rootButton != null)
{
UTKTooltipManager.Instance.DetachTooltip(_rootButton);
}
// 테마 구독 해제
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);