324 lines
11 KiB
C#
324 lines
11 KiB
C#
#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; 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;
|
|
}
|
|
}
|
|
}
|