640 lines
25 KiB
C#
640 lines
25 KiB
C#
#nullable enable
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace UVC.UIToolkit
|
||
{
|
||
/// <summary>
|
||
/// 단축키 설정 리스트 컴포넌트.
|
||
/// Command 이름, Ctrl / Shift / Alt 체크박스, Key 입력(읽기전용 + 클릭 시 키 캡처)으로 구성됩니다.
|
||
/// </summary>
|
||
///
|
||
/// <remarks>
|
||
/// <para><b>열 구성 (왼쪽 → 오른쪽):</b></para>
|
||
/// <list type="bullet">
|
||
/// <item>Command 이름 (flex-grow)</item>
|
||
/// <item>Ctrl 체크박스 (52 px)</item>
|
||
/// <item>Shift 체크박스 (52 px)</item>
|
||
/// <item>Alt 체크박스 (52 px)</item>
|
||
/// <item>Key 입력 필드 (76 px) – 클릭 시 다음 키 자동 캡처</item>
|
||
/// </list>
|
||
///
|
||
/// <para><b>Key 캡처 방법:</b></para>
|
||
/// <list type="number">
|
||
/// <item>Key 필드 클릭 → 캡처 모드 진입 ("···" 표시)</item>
|
||
/// <item>원하는 키 입력 → 자동 저장 후 캡처 종료</item>
|
||
/// <item>Escape 키 → 취소 (이전 값 복원)</item>
|
||
/// </list>
|
||
///
|
||
/// <para><b>가상화:</b> UTKListView(ListView) 를 사용하여 대량 항목도 성능 저하 없이 표시합니다.</para>
|
||
///
|
||
/// <para><b>관련 리소스:</b></para>
|
||
/// <list type="bullet">
|
||
/// <item>Resources/UIToolkit/List/UTKShortcutList.uxml</item>
|
||
/// <item>Resources/UIToolkit/List/UTKShortcutListItem.uxml</item>
|
||
/// <item>Resources/UIToolkit/List/UTKShortcutListUss.uss</item>
|
||
/// </list>
|
||
/// </remarks>
|
||
///
|
||
/// <example>
|
||
/// <code>
|
||
/// var list = new UTKShortcutList();
|
||
/// list.OnDataChanged += (item) => Debug.Log($"변경: {item.CommandName}");
|
||
///
|
||
/// list.SetData(new List<UTKShortcutItemData>
|
||
/// {
|
||
/// new() { Id = "file.new", CommandName = "File > New Project", UseCtrl = true, Key = "N" },
|
||
/// new() { Id = "edit.undo", CommandName = "Edit > Undo", UseCtrl = true, Key = "Z" },
|
||
/// });
|
||
/// </code>
|
||
/// </example>
|
||
[UxmlElement]
|
||
public partial class UTKShortcutList : VisualElement, IDisposable
|
||
{
|
||
#region Constants
|
||
private const string UXML_PATH = "UIToolkit/List/UTKShortcutList";
|
||
private const string USS_PATH = "UIToolkit/List/UTKShortcutListUss";
|
||
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKShortcutListItem";
|
||
private const float ITEM_HEIGHT = 36f;
|
||
#endregion
|
||
|
||
#region Fields
|
||
private bool _disposed;
|
||
|
||
private UTKListView? _listView;
|
||
private UTKInputField? _searchField;
|
||
private UTKButton? _clearButton;
|
||
|
||
// 전체 데이터 · 검색 필터링된 데이터 분리
|
||
private List<UTKShortcutItemData> _allItems = new();
|
||
private readonly List<UTKShortcutItemData> _filteredItems = new();
|
||
|
||
// UXML 캐싱 (makeItem 호출마다 Resources.Load 방지)
|
||
private VisualTreeAsset? _itemTemplate;
|
||
#endregion
|
||
|
||
#region Events
|
||
/// <summary>
|
||
/// 단축키 데이터가 변경될 때 발생합니다.
|
||
/// 변경된 <see cref="UTKShortcutItemData"/> 인스턴스를 전달합니다.
|
||
/// </summary>
|
||
public event Action<UTKShortcutItemData>? OnDataChanged;
|
||
#endregion
|
||
|
||
#region Constructor
|
||
public UTKShortcutList() : base()
|
||
{
|
||
// 1. 테마 적용
|
||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||
|
||
// 2. USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨)
|
||
var uss = Resources.Load<StyleSheet>(USS_PATH);
|
||
if (uss != null)
|
||
styleSheets.Add(uss);
|
||
else
|
||
Debug.LogWarning($"[UTKShortcutList] USS not found: {USS_PATH}");
|
||
|
||
// 3. UXML 로드 → 요소 구성
|
||
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||
if (visualTree != null)
|
||
{
|
||
visualTree.CloneTree(this);
|
||
InitializeFromUxml();
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning($"[UTKShortcutList] UXML not found: {UXML_PATH}, using fallback");
|
||
CreateFallbackUI();
|
||
}
|
||
|
||
// 4. 테마 변경 구독
|
||
SubscribeToThemeChanges();
|
||
}
|
||
#endregion
|
||
|
||
#region UI Creation
|
||
|
||
/// <summary>UXML 로드 성공 시 – 자식 요소 참조 획득 및 초기화.</summary>
|
||
private void InitializeFromUxml()
|
||
{
|
||
AddToClassList("utk-shortcut-list");
|
||
|
||
_searchField = this.Q<UTKInputField>("search-field");
|
||
_clearButton = this.Q<UTKButton>("clear-btn");
|
||
_listView = this.Q<UTKListView>("list-view");
|
||
|
||
BindSearchField();
|
||
SetupListView();
|
||
}
|
||
|
||
/// <summary>UXML 로드 실패 시 – 코드로 UI 구성.</summary>
|
||
private void CreateFallbackUI()
|
||
{
|
||
AddToClassList("utk-shortcut-list");
|
||
|
||
// 검색 영역
|
||
var searchContainer = new VisualElement { name = "search-container" };
|
||
searchContainer.AddToClassList("utk-shortcut-list__search-container");
|
||
Add(searchContainer);
|
||
|
||
_searchField = new UTKInputField { name = "search-field" };
|
||
_searchField.AddToClassList("utk-shortcut-list__search");
|
||
searchContainer.Add(_searchField);
|
||
|
||
_clearButton = new UTKButton { name = "clear-btn" };
|
||
_clearButton.Variant = UTKButton.ButtonVariant.Text;
|
||
_clearButton.SetMaterialIcon(UTKMaterialIcons.Close, 12);
|
||
_clearButton.AddToClassList("utk-shortcut-list__clear-btn");
|
||
searchContainer.Add(_clearButton);
|
||
|
||
// 컬럼 헤더
|
||
var header = new VisualElement { name = "header" };
|
||
header.AddToClassList("utk-shortcut-list__header");
|
||
header.Add(MakeHeaderLabel("", "utk-shortcut-list__header-command"));
|
||
header.Add(MakeHeaderLabel("Ctrl", "utk-shortcut-list__header-modifier"));
|
||
header.Add(MakeHeaderLabel("Shift", "utk-shortcut-list__header-modifier"));
|
||
header.Add(MakeHeaderLabel("Alt", "utk-shortcut-list__header-modifier"));
|
||
header.Add(MakeHeaderLabel("Key", "utk-shortcut-list__header-key"));
|
||
Add(header);
|
||
|
||
// ListView
|
||
_listView = new UTKListView { name = "list-view" };
|
||
_listView.AddToClassList("utk-shortcut-list__listview");
|
||
Add(_listView);
|
||
|
||
BindSearchField();
|
||
SetupListView();
|
||
}
|
||
|
||
private static Label MakeHeaderLabel(string text, string className)
|
||
{
|
||
var label = new Label(text);
|
||
label.AddToClassList(className);
|
||
return label;
|
||
}
|
||
|
||
/// <summary>검색 필드 이벤트 연결.</summary>
|
||
private void BindSearchField()
|
||
{
|
||
// Clear 버튼 초기 숨김
|
||
if (_clearButton != null)
|
||
{
|
||
_clearButton.style.display = DisplayStyle.None;
|
||
_clearButton.OnClicked += OnClearButtonClicked;
|
||
}
|
||
|
||
if (_searchField == null) return;
|
||
// Enter 키 또는 포커스 잃을 때 검색 실행 (UTKComponentList 방식)
|
||
_searchField.OnSubmit += OnSearch;
|
||
}
|
||
|
||
/// <summary>ListView makeItem / bindItem / unbindItem 설정.</summary>
|
||
private void SetupListView()
|
||
{
|
||
if (_listView == null) return;
|
||
|
||
_listView.makeItem = MakeItem;
|
||
_listView.bindItem = BindItem;
|
||
_listView.unbindItem = UnbindItem;
|
||
_listView.fixedItemHeight = ITEM_HEIGHT;
|
||
_listView.selectionType = SelectionType.None;
|
||
_listView.itemsSource = _filteredItems;
|
||
}
|
||
#endregion
|
||
|
||
#region Theme
|
||
private void SubscribeToThemeChanges()
|
||
{
|
||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||
RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||
}
|
||
|
||
private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
|
||
{
|
||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||
}
|
||
|
||
private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
|
||
{
|
||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||
}
|
||
|
||
private void OnThemeChanged(UTKTheme theme)
|
||
{
|
||
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
||
}
|
||
#endregion
|
||
|
||
#region Search
|
||
/// <summary>Clear 버튼 클릭 처리 – 검색어 초기화 및 재검색.</summary>
|
||
private void OnClearButtonClicked()
|
||
{
|
||
if (_searchField != null && _searchField.value.Length > 0)
|
||
{
|
||
_searchField.value = string.Empty;
|
||
OnSearch(string.Empty);
|
||
}
|
||
if (_clearButton != null)
|
||
_clearButton.style.display = DisplayStyle.None;
|
||
}
|
||
|
||
/// <summary>검색 실행 (Enter 키 또는 포커스 잃을 때 호출).</summary>
|
||
private void OnSearch(string query)
|
||
{
|
||
// Clear 버튼 표시/숨김
|
||
if (_clearButton != null)
|
||
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
|
||
|
||
_filteredItems.Clear();
|
||
|
||
if (string.IsNullOrWhiteSpace(query))
|
||
{
|
||
_filteredItems.AddRange(_allItems);
|
||
}
|
||
else
|
||
{
|
||
var lower = query.ToLowerInvariant();
|
||
foreach (var item in _allItems)
|
||
{
|
||
if (item.CommandName.ToLowerInvariant().Contains(lower))
|
||
_filteredItems.Add(item);
|
||
}
|
||
}
|
||
|
||
_listView?.RefreshItems();
|
||
}
|
||
#endregion
|
||
|
||
#region ListView Callbacks
|
||
|
||
/// <summary>
|
||
/// 아이템 VisualElement 생성 (가상화 재사용 요소).
|
||
/// UXML을 캐싱하여 매 호출마다 Resources.Load 를 방지합니다.
|
||
/// </summary>
|
||
private VisualElement MakeItem()
|
||
{
|
||
_itemTemplate ??= Resources.Load<VisualTreeAsset>(ITEM_UXML_PATH);
|
||
|
||
if (_itemTemplate != null)
|
||
return _itemTemplate.Instantiate();
|
||
|
||
Debug.LogWarning($"[UTKShortcutList] Item UXML not found: {ITEM_UXML_PATH}, using fallback");
|
||
return CreateItemFallback();
|
||
}
|
||
|
||
/// <summary>UXML 로드 실패 시 코드로 아이템 행 생성.</summary>
|
||
private static VisualElement CreateItemFallback()
|
||
{
|
||
var container = new VisualElement { name = "item-container" };
|
||
container.AddToClassList("utk-shortcut-list-item");
|
||
|
||
var cmd = new UTKLabel { name = "command-label" };
|
||
cmd.AddToClassList("utk-shortcut-list-item__command");
|
||
container.Add(cmd);
|
||
|
||
foreach (var nm in new[] { "ctrl-checkbox", "shift-checkbox", "alt-checkbox" })
|
||
{
|
||
var cb = new UTKCheckBox { name = nm };
|
||
cb.AddToClassList("utk-shortcut-list-item__modifier");
|
||
container.Add(cb);
|
||
}
|
||
|
||
var key = new UTKInputField { name = "key-field" };
|
||
key.AddToClassList("utk-shortcut-list-item__key");
|
||
container.Add(key);
|
||
|
||
return container;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 데이터를 VisualElement 에 바인딩합니다.
|
||
/// 이전 콜백을 해제하고 새 콜백을 등록하여 중복 이벤트를 방지합니다.
|
||
/// </summary>
|
||
private void BindItem(VisualElement element, int index)
|
||
{
|
||
if (index < 0 || index >= _filteredItems.Count) return;
|
||
|
||
// ListView 가 내부적으로 flex-grow: 0 을 인라인으로 강제하므로 덮어씁니다.
|
||
element.style.flexGrow = 1;
|
||
|
||
var data = _filteredItems[index];
|
||
|
||
// 요소 참조 획득
|
||
var root = element.Q<VisualElement>("item-container");
|
||
var cmdLabel = root?.Q<UTKLabel>("command-label");
|
||
var ctrlBox = root?.Q<UTKCheckBox>("ctrl-checkbox");
|
||
var shiftBox = root?.Q<UTKCheckBox>("shift-checkbox");
|
||
var altBox = root?.Q<UTKCheckBox>("alt-checkbox");
|
||
var keyField = root?.Q<UTKInputField>("key-field");
|
||
|
||
if (cmdLabel == null || ctrlBox == null || shiftBox == null || altBox == null || keyField == null)
|
||
{
|
||
Debug.LogWarning("[UTKShortcutList] BindItem: 일부 자식 요소를 찾을 수 없습니다.");
|
||
return;
|
||
}
|
||
|
||
// 이전 바인딩 해제
|
||
CleanupItemCallbacks(element);
|
||
|
||
// ── 값 설정 (notify: false → 이벤트 미발생) ──────────────
|
||
cmdLabel.Text = data.CommandName;
|
||
ctrlBox.SetChecked(data.UseCtrl, notify: false);
|
||
shiftBox.SetChecked(data.UseShift, notify: false);
|
||
altBox.SetChecked(data.UseAlt, notify: false);
|
||
keyField.SetValue(data.Key, notify: false);
|
||
keyField.isReadOnly = true; // 직접 타이핑 방지 (캡처만으로 설정)
|
||
|
||
// ── 수정자 키 체크박스 콜백 ──────────────────────────────
|
||
Action<bool> onCtrl = v => { data.UseCtrl = v; OnDataChanged?.Invoke(data); };
|
||
Action<bool> onShift = v => { data.UseShift = v; OnDataChanged?.Invoke(data); };
|
||
Action<bool> onAlt = v => { data.UseAlt = v; OnDataChanged?.Invoke(data); };
|
||
|
||
ctrlBox.OnValueChanged += onCtrl;
|
||
shiftBox.OnValueChanged += onShift;
|
||
altBox.OnValueChanged += onAlt;
|
||
|
||
// ── Key 캡처 콜백 ─────────────────────────────────────────
|
||
var capture = new KeyCaptureState(data, keyField);
|
||
|
||
EventCallback<FocusInEvent> onFocusIn = _ => capture.StartCapture();
|
||
EventCallback<KeyDownEvent> onKeyDown = evt => capture.HandleKeyDown(evt, () => OnDataChanged?.Invoke(data));
|
||
EventCallback<FocusOutEvent> onFocusOut = _ => capture.CancelCapture();
|
||
|
||
keyField.RegisterCallback(onFocusIn);
|
||
// TrickleDown 으로 등록 → 내부 TextElement 가 키 이벤트를 받기 전에 가로채기
|
||
keyField.RegisterCallback(onKeyDown, TrickleDown.TrickleDown);
|
||
keyField.RegisterCallback(onFocusOut);
|
||
|
||
// 해제 정보 저장
|
||
element.userData = new ShortcutItemCallbackInfo(
|
||
ctrlBox, shiftBox, altBox, keyField,
|
||
onCtrl, onShift, onAlt,
|
||
onFocusIn, onKeyDown, onFocusOut);
|
||
}
|
||
|
||
/// <summary>가상화 재사용 전 콜백 정리.</summary>
|
||
private void UnbindItem(VisualElement element, int index)
|
||
{
|
||
CleanupItemCallbacks(element);
|
||
}
|
||
|
||
/// <summary>element.userData 에 저장된 모든 이벤트 콜백을 해제합니다.</summary>
|
||
private static void CleanupItemCallbacks(VisualElement element)
|
||
{
|
||
if (element.userData is not ShortcutItemCallbackInfo info) return;
|
||
|
||
info.CtrlBox.OnValueChanged -= info.OnCtrlHandler;
|
||
info.ShiftBox.OnValueChanged -= info.OnShiftHandler;
|
||
info.AltBox.OnValueChanged -= info.OnAltHandler;
|
||
|
||
info.KeyField.UnregisterCallback(info.OnFocusIn);
|
||
info.KeyField.UnregisterCallback(info.OnKeyDown, TrickleDown.TrickleDown);
|
||
info.KeyField.UnregisterCallback(info.OnFocusOut);
|
||
|
||
element.userData = null;
|
||
}
|
||
#endregion
|
||
|
||
#region Public API
|
||
|
||
/// <summary>
|
||
/// 단축키 목록을 설정하고 ListView 를 갱신합니다.
|
||
/// </summary>
|
||
/// <param name="items">표시할 단축키 데이터 목록.</param>
|
||
public void SetData(List<UTKShortcutItemData> items)
|
||
{
|
||
_allItems = items ?? new List<UTKShortcutItemData>();
|
||
OnSearch(_searchField?.value ?? string.Empty);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 현재 단축키 목록(원본 전체)을 반환합니다.
|
||
/// </summary>
|
||
/// <returns>복사본 목록.</returns>
|
||
public List<UTKShortcutItemData> GetData() => new(_allItems);
|
||
|
||
/// <summary>
|
||
/// ListView 를 강제로 새로고침합니다.
|
||
/// 외부에서 데이터를 직접 변경한 후 호출하세요.
|
||
/// </summary>
|
||
public void RefreshItems() => _listView?.RefreshItems();
|
||
|
||
#endregion
|
||
|
||
#region Internal Types
|
||
|
||
/// <summary>
|
||
/// Key 캡처 상태 관리.
|
||
/// FocusIn → 캡처 시작, KeyDown → 키 저장, FocusOut/Escape → 취소.
|
||
/// </summary>
|
||
private sealed class KeyCaptureState
|
||
{
|
||
private readonly UTKShortcutItemData _data;
|
||
private readonly UTKInputField _keyField;
|
||
private bool _isCapturing;
|
||
private string _originalKey = "";
|
||
|
||
/// <summary>캡처 대기 중 표시 문자열.</summary>
|
||
private const string CAPTURE_PLACEHOLDER = "···";
|
||
|
||
public KeyCaptureState(UTKShortcutItemData data, UTKInputField keyField)
|
||
{
|
||
_data = data;
|
||
_keyField = keyField;
|
||
}
|
||
|
||
/// <summary>캡처 모드 진입 – 대기 표시 문자열로 교체.</summary>
|
||
public void StartCapture()
|
||
{
|
||
if (_isCapturing) return;
|
||
|
||
_isCapturing = true;
|
||
_originalKey = _data.Key;
|
||
|
||
_keyField.AddToClassList("utk-shortcut-list-item__key--capturing");
|
||
_keyField.SetValue(CAPTURE_PLACEHOLDER, notify: false);
|
||
}
|
||
|
||
/// <summary>캡처 취소 – 원래 값 복원.</summary>
|
||
public void CancelCapture()
|
||
{
|
||
if (!_isCapturing) return;
|
||
|
||
_isCapturing = false;
|
||
_keyField.RemoveFromClassList("utk-shortcut-list-item__key--capturing");
|
||
_keyField.SetValue(_originalKey, notify: false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// KeyDown 이벤트 처리.
|
||
/// <list type="bullet">
|
||
/// <item>Escape → 캡처 취소.</item>
|
||
/// <item>수정자 키(Ctrl/Shift/Alt 단독) → 무시.</item>
|
||
/// <item>그 외 → 키 이름 저장 후 캡처 종료.</item>
|
||
/// </list>
|
||
/// </summary>
|
||
public void HandleKeyDown(KeyDownEvent evt, Action onChanged)
|
||
{
|
||
if (!_isCapturing) return;
|
||
|
||
var code = evt.keyCode;
|
||
|
||
// Escape: 취소
|
||
if (code == KeyCode.Escape)
|
||
{
|
||
evt.StopImmediatePropagation();
|
||
CancelCapture();
|
||
_keyField.Blur();
|
||
return;
|
||
}
|
||
|
||
// 수정자 키 단독 입력 → 무시 (Ctrl/Shift/Alt 체크박스로 설정)
|
||
if (code is KeyCode.LeftControl or KeyCode.RightControl or
|
||
KeyCode.LeftShift or KeyCode.RightShift or
|
||
KeyCode.LeftAlt or KeyCode.RightAlt or
|
||
KeyCode.LeftCommand or KeyCode.RightCommand or
|
||
KeyCode.None)
|
||
return;
|
||
|
||
// 키 저장
|
||
_isCapturing = false;
|
||
_keyField.RemoveFromClassList("utk-shortcut-list-item__key--capturing");
|
||
|
||
var keyName = ResolveKeyName(code, evt.character);
|
||
_data.Key = keyName;
|
||
_keyField.SetValue(keyName, notify: false);
|
||
|
||
onChanged.Invoke();
|
||
|
||
evt.StopImmediatePropagation();
|
||
evt.PreventDefault();
|
||
|
||
_keyField.Blur();
|
||
}
|
||
|
||
/// <summary>KeyCode → 표시 문자열 변환.</summary>
|
||
private static string ResolveKeyName(KeyCode code, char character)
|
||
{
|
||
return code switch
|
||
{
|
||
KeyCode.Delete => "Delete",
|
||
KeyCode.Backspace => "Backspace",
|
||
KeyCode.Return => "Enter",
|
||
KeyCode.KeypadEnter => "Enter",
|
||
KeyCode.Tab => "Tab",
|
||
KeyCode.Space => "Space",
|
||
KeyCode.Insert => "Insert",
|
||
KeyCode.Home => "Home",
|
||
KeyCode.End => "End",
|
||
KeyCode.PageUp => "PgUp",
|
||
KeyCode.PageDown => "PgDn",
|
||
KeyCode.UpArrow => "↑",
|
||
KeyCode.DownArrow => "↓",
|
||
KeyCode.LeftArrow => "←",
|
||
KeyCode.RightArrow => "→",
|
||
KeyCode.F1 => "F1",
|
||
KeyCode.F2 => "F2",
|
||
KeyCode.F3 => "F3",
|
||
KeyCode.F4 => "F4",
|
||
KeyCode.F5 => "F5",
|
||
KeyCode.F6 => "F6",
|
||
KeyCode.F7 => "F7",
|
||
KeyCode.F8 => "F8",
|
||
KeyCode.F9 => "F9",
|
||
KeyCode.F10 => "F10",
|
||
KeyCode.F11 => "F11",
|
||
KeyCode.F12 => "F12",
|
||
KeyCode.Keypad0 => "Num0",
|
||
KeyCode.Keypad1 => "Num1",
|
||
KeyCode.Keypad2 => "Num2",
|
||
KeyCode.Keypad3 => "Num3",
|
||
KeyCode.Keypad4 => "Num4",
|
||
KeyCode.Keypad5 => "Num5",
|
||
KeyCode.Keypad6 => "Num6",
|
||
KeyCode.Keypad7 => "Num7",
|
||
KeyCode.Keypad8 => "Num8",
|
||
KeyCode.Keypad9 => "Num9",
|
||
KeyCode.KeypadPlus => "Num+",
|
||
KeyCode.KeypadMinus => "Num-",
|
||
KeyCode.KeypadMultiply => "Num*",
|
||
KeyCode.KeypadDivide => "Num/",
|
||
KeyCode.KeypadPeriod => "Num.",
|
||
// 일반 문자 키 – character 우선, 없으면 KeyCode.ToString()
|
||
_ => character != '\0' && !char.IsControl(character)
|
||
? character.ToString().ToUpperInvariant()
|
||
: code.ToString()
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 아이템 바인딩 시 등록하는 이벤트 콜백 참조 보관.
|
||
/// UnbindItem 에서 정확히 해제하기 위해 사용합니다.
|
||
/// </summary>
|
||
private sealed class ShortcutItemCallbackInfo
|
||
{
|
||
public readonly UTKCheckBox CtrlBox;
|
||
public readonly UTKCheckBox ShiftBox;
|
||
public readonly UTKCheckBox AltBox;
|
||
public readonly UTKInputField KeyField;
|
||
|
||
public readonly Action<bool> OnCtrlHandler;
|
||
public readonly Action<bool> OnShiftHandler;
|
||
public readonly Action<bool> OnAltHandler;
|
||
|
||
public readonly EventCallback<FocusInEvent> OnFocusIn;
|
||
public readonly EventCallback<KeyDownEvent> OnKeyDown;
|
||
public readonly EventCallback<FocusOutEvent> OnFocusOut;
|
||
|
||
public ShortcutItemCallbackInfo(
|
||
UTKCheckBox ctrl, UTKCheckBox shift, UTKCheckBox alt, UTKInputField key,
|
||
Action<bool> onCtrl, Action<bool> onShift, Action<bool> onAlt,
|
||
EventCallback<FocusInEvent> onFocusIn,
|
||
EventCallback<KeyDownEvent> onKeyDown,
|
||
EventCallback<FocusOutEvent> onFocusOut)
|
||
{
|
||
CtrlBox = ctrl; ShiftBox = shift; AltBox = alt; KeyField = key;
|
||
OnCtrlHandler = onCtrl; OnShiftHandler = onShift; OnAltHandler = onAlt;
|
||
OnFocusIn = onFocusIn;
|
||
OnKeyDown = onKeyDown;
|
||
OnFocusOut = onFocusOut;
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region IDisposable
|
||
|
||
public void Dispose()
|
||
{
|
||
if (_disposed) return;
|
||
_disposed = true;
|
||
|
||
// 테마 구독 해제
|
||
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
||
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
|
||
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
|
||
|
||
// 검색 필드 이벤트 해제
|
||
if (_searchField != null)
|
||
_searchField.OnSubmit -= OnSearch;
|
||
|
||
// Clear 버튼 이벤트 해제
|
||
if (_clearButton != null)
|
||
_clearButton.OnClicked -= OnClearButtonClicked;
|
||
|
||
// ListView 정리
|
||
_listView?.Dispose();
|
||
|
||
// 이벤트 · 캐시 정리
|
||
OnDataChanged = null;
|
||
_itemTemplate = null;
|
||
}
|
||
#endregion
|
||
}
|
||
}
|