Files

324 lines
11 KiB
C#
Raw Permalink Normal View History

2026-03-10 11:35:30 +09:00
#nullable enable
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 코드 샘플을 표시하는 컴포넌트.
/// 텍스트 선택 및 복사가 가능하며, 복사 버튼을 제공합니다.
/// </summary>
/// <remarks>
/// <para><b>CodeBlock(코드 블록)이란?</b></para>
/// <para>
/// CodeBlock은 코드 샘플을 읽기 쉽게 표시하는 컴포넌트입니다.
/// 고정폭 폰트, 배경색, 복사 버튼 등으로 코드를 강조하여 표시합니다.
/// 문서, 튜토리얼, 샘플 앱 등에서 코드 예제를 보여줄 때 사용합니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>고정폭 폰트로 코드 가독성 향상</description></item>
/// <item><description>텍스트 선택 및 복사 가능</description></item>
/// <item><description>복사 버튼으로 원클릭 복사</description></item>
/// <item><description>제목 표시 (예: "C#", "UXML", "JSON")</description></item>
/// </list>
///
/// <para><b>UXML에서 코드 설정 방법:</b></para>
/// <list type="number">
/// <item><description>code 속성 사용: <c>code="var x = 1;\nvar y = 2;"</c> (줄바꿈은 \n)</description></item>
/// <item><description>텍스트 콘텐츠 사용: 태그 사이에 직접 코드 작성 (줄바꿈 유지)</description></item>
/// </list>
///
/// <para><b>실제 활용 예시:</b></para>
/// <list type="bullet">
/// <item><description>API 문서 - 함수 사용법 예제</description></item>
/// <item><description>튜토리얼 - 단계별 코드 설명</description></item>
/// <item><description>StyleGuide 샘플 - C#/UXML 코드 예시</description></item>
/// <item><description>오류 로그 - 스택 트레이스 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>UXML에서 사용 (code 속성):</b></para>
/// <code>
/// <utk:UTKCodeBlock title="C#" code="var x = 1;&#10;var y = 2;" />
/// </code>
/// <para><b>UXML에서 사용 (텍스트 콘텐츠):</b></para>
/// <code>
/// <utk:UTKCodeBlock title="C#">
/// var x = 1;
/// var y = 2;
/// </utk:UTKCodeBlock>
/// </code>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
/// var codeBlock = new UTKCodeBlock
/// {
/// Title = "C#",
/// Code = "var player = new Player();\nplayer.Move(10, 0);"
/// };
///
/// // 여러 코드 블록 컨테이너 생성
/// var container = UTKCodeBlock.CreateCodeSampleContainer(
/// csharpCode: "var btn = new UTKButton();",
/// uxmlCode: "<utk:UTKButton text=\"Click\" />"
/// );
/// </code>
/// </example>
[UxmlElement]
public partial class UTKCodeBlock : VisualElement
{
private static class UssClasses
{
public const string Root = "utk-code-block";
public const string Header = "utk-code-block__header";
public const string Title = "utk-code-block__title";
public const string CopyButton = "utk-code-block__copy-btn";
public const string Content = "utk-code-block__content";
public const string Code = "utk-code-block__code";
}
private Label? _titleLabel;
private UTKButton? _copyButton;
private TextField? _codeField;
private string _title = "";
private string _code = "";
private bool _codeSetFromAttribute;
/// <summary>
/// 코드 블록 제목 (예: "C# Code", "UXML")
/// </summary>
[UxmlAttribute("title")]
public string Title
{
get => _title;
set
{
_title = value;
if (_titleLabel != null)
{
_titleLabel.text = value;
_titleLabel.style.display = string.IsNullOrEmpty(value) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
}
/// <summary>
/// 표시할 코드
/// UXML에서 줄바꿈은 \n 문자열로 표현하거나, 텍스트 콘텐츠로 직접 입력할 수 있습니다.
/// </summary>
[UxmlAttribute("code")]
public string Code
{
get => _code;
set
{
// UXML에서 "\n" 문자열 리터럴이 들어올 수 있으므로 실제 줄바꿈으로 변환
_code = (value ?? "").Replace("\\n", "\n");
_codeSetFromAttribute = !string.IsNullOrEmpty(_code);
if (_codeField != null)
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
}
}
/// <summary>
/// 텍스트 콘텐츠로 코드를 설정합니다.
/// code 속성이 설정되지 않은 경우에만 적용됩니다.
/// </summary>
public void SetCodeFromContent(string content)
{
if (_codeSetFromAttribute) return;
// 앞뒤 빈 줄 제거하고 공통 들여쓰기 제거
_code = TrimAndNormalizeIndent(content);
if (_codeField != null)
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
}
/// <summary>
/// 텍스트의 앞뒤 빈 줄을 제거하고 공통 들여쓰기를 정규화합니다.
/// </summary>
private static string TrimAndNormalizeIndent(string text)
{
if (string.IsNullOrEmpty(text)) return "";
var lines = text.Split('\n');
// 앞뒤 빈 줄 제거
int startIndex = 0;
int endIndex = lines.Length - 1;
while (startIndex <= endIndex && string.IsNullOrWhiteSpace(lines[startIndex]))
startIndex++;
while (endIndex >= startIndex && string.IsNullOrWhiteSpace(lines[endIndex]))
endIndex--;
if (startIndex > endIndex) return "";
// 공통 들여쓰기 찾기
int minIndent = int.MaxValue;
for (int i = startIndex; i <= endIndex; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line)) continue;
int indent = 0;
foreach (char c in line)
{
if (c == ' ') indent++;
else if (c == '\t') indent += 4;
else break;
}
minIndent = Mathf.Min(minIndent, indent);
}
if (minIndent == int.MaxValue) minIndent = 0;
// 공통 들여쓰기 제거하고 결과 생성
var result = new System.Text.StringBuilder();
for (int i = startIndex; i <= endIndex; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
result.AppendLine();
}
else
{
int removed = 0;
int charIndex = 0;
while (removed < minIndent && charIndex < line.Length)
{
if (line[charIndex] == ' ') { removed++; charIndex++; }
else if (line[charIndex] == '\t') { removed += 4; charIndex++; }
else break;
}
result.AppendLine(line[charIndex..]);
}
}
return result.ToString().TrimEnd('\r', '\n');
}
private void UpdateCodeFieldHeight()
{
if (_codeField == null) return;
// 줄 수에 따라 높이 조정
var lineCount = string.IsNullOrEmpty(_code) ? 1 : _code.Split('\n').Length;
var height = Mathf.Max(40, lineCount * 18 + 24);
_codeField.style.height = height;
}
public UTKCodeBlock()
{
AddToClassList(UssClasses.Root);
// Header
var header = new VisualElement();
header.AddToClassList(UssClasses.Header);
Add(header);
_titleLabel = new Label();
_titleLabel.AddToClassList(UssClasses.Title);
header.Add(_titleLabel);
_copyButton = new UTKButton("Copy", UTKMaterialIcons.ContentCopy, UTKButton.ButtonVariant.Text, 12);
_copyButton.AddToClassList(UssClasses.CopyButton);
_copyButton.OnClicked += OnCopyClicked;
header.Add(_copyButton);
// Content
var content = new VisualElement();
content.AddToClassList(UssClasses.Content);
Add(content);
// TextField를 사용하여 텍스트 선택 가능하게
_codeField = new TextField();
_codeField.AddToClassList(UssClasses.Code);
_codeField.multiline = true;
_codeField.isReadOnly = true;
content.Add(_codeField);
// 초기값 적용 (UXML에서 속성이 먼저 설정된 경우)
if (!string.IsNullOrEmpty(_title))
{
_titleLabel.text = _title;
}
if (!string.IsNullOrEmpty(_code))
{
_codeField.value = _code;
UpdateCodeFieldHeight();
}
// UXML 텍스트 콘텐츠 처리를 위한 콜백
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
// UXML에서 텍스트 콘텐츠가 있으면 처리
// Unity는 텍스트 콘텐츠를 Label 자식으로 추가하므로 확인
schedule.Execute(() =>
{
if (_codeSetFromAttribute) return;
// 자식 Label에서 텍스트 추출 시도
var textLabel = this.Q<Label>(className: null);
if (textLabel != null && textLabel != _titleLabel && !string.IsNullOrEmpty(textLabel.text))
{
SetCodeFromContent(textLabel.text);
textLabel.RemoveFromHierarchy();
}
});
}
private void OnCopyClicked()
{
if (!string.IsNullOrEmpty(_code))
{
GUIUtility.systemCopyBuffer = _code;
UTKToast.Show("코드가 클립보드에 복사되었습니다.");
}
}
/// <summary>
/// C# 코드와 UXML 코드를 함께 표시하는 컨테이너를 생성합니다.
/// </summary>
public static VisualElement CreateCodeSampleContainer(string? csharpCode, string? uxmlCode)
{
var container = new VisualElement();
container.AddToClassList("utk-code-sample-container");
if (!string.IsNullOrEmpty(csharpCode))
{
var csharpBlock = new UTKCodeBlock
{
Title = "C#",
Code = csharpCode
};
container.Add(csharpBlock);
}
if (!string.IsNullOrEmpty(uxmlCode))
{
var uxmlBlock = new UTKCodeBlock
{
Title = "UXML",
Code = uxmlCode
};
container.Add(uxmlBlock);
}
return container;
}
}
}