473 lines
14 KiB
C#
473 lines
14 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace SHI.Modal
|
|
{
|
|
/// <summary>
|
|
/// 시간 기반 재생을 제어하는 UI Toolkit 컴포넌트입니다.
|
|
/// </summary>
|
|
[UxmlElement]
|
|
public partial class PlayBar : VisualElement, IDisposable
|
|
{
|
|
#region 상수 (Constants)
|
|
private const string UXML_PATH = "SHI/Modal/PlayBar";
|
|
private const int DEFAULT_INTERVAL_SECONDS = 2;
|
|
#endregion
|
|
|
|
#region UI 컴포넌트 참조
|
|
private Label? _startLabel;
|
|
private Label? _endLabel;
|
|
private Label? _currentTimeLabel;
|
|
private VisualElement? _progressTrack;
|
|
private VisualElement? _progressFill;
|
|
private Button? _playBtn;
|
|
private Button? _firstBtn;
|
|
private Button? _lastBtn;
|
|
private Button? _stopBtn;
|
|
private DropdownField? _intervalDropdown;
|
|
#endregion
|
|
|
|
#region 상태 (State)
|
|
private DateTime _startTime;
|
|
private DateTime _endTime;
|
|
private DateTime _currentTime;
|
|
private bool _isPlaying;
|
|
private int _intervalSeconds = DEFAULT_INTERVAL_SECONDS;
|
|
private IVisualElementScheduledItem? _playSchedule;
|
|
private ProgressDragManipulator? _dragManipulator;
|
|
#endregion
|
|
|
|
#region 외부 이벤트 (Public Events)
|
|
/// <summary>재생이 시작될 때 발생</summary>
|
|
public event Action? OnPlayStarted;
|
|
|
|
/// <summary>재생이 정지될 때 발생</summary>
|
|
public event Action? OnPlayStopped;
|
|
|
|
/// <summary>재생 중 시간이 변경될 때 발생 (자동 재생 시)</summary>
|
|
public event Action<DateTime>? OnPlayProgress;
|
|
|
|
/// <summary>사용자가 진행바를 드래그/클릭하여 위치를 변경할 때 발생</summary>
|
|
public event Action<DateTime>? OnPositionChanged;
|
|
#endregion
|
|
|
|
#region UxmlAttribute
|
|
[UxmlAttribute]
|
|
public bool IsVisible
|
|
{
|
|
get => style.display == DisplayStyle.Flex;
|
|
set => style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
#endregion
|
|
|
|
#region 생성자 (Constructor)
|
|
public PlayBar()
|
|
{
|
|
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
Debug.Log("[PlayBar] UXML loaded and cloned." + (visualTree == null));
|
|
if (visualTree == null)
|
|
{
|
|
Debug.LogError($"[PlayBar] UXML not found at: {UXML_PATH}");
|
|
return;
|
|
}
|
|
visualTree.CloneTree(this);
|
|
InitializeUIReferences();
|
|
InitializeEventHandlers();
|
|
|
|
// 패널에 연결된 후 드롭다운 초기화
|
|
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
|
}
|
|
|
|
private void OnAttachToPanel(AttachToPanelEvent evt)
|
|
{
|
|
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
|
InitializeDropdown();
|
|
}
|
|
#endregion
|
|
|
|
#region 초기화 (Initialization)
|
|
private void InitializeUIReferences()
|
|
{
|
|
_startLabel = this.Q<Label>("start-label");
|
|
_endLabel = this.Q<Label>("end-label");
|
|
_currentTimeLabel = this.Q<Label>("current-time-label");
|
|
_progressTrack = this.Q<VisualElement>("progress-track");
|
|
_progressFill = this.Q<VisualElement>("progress-fill");
|
|
_playBtn = this.Q<Button>("play-btn");
|
|
_firstBtn = this.Q<Button>("first-btn");
|
|
_lastBtn = this.Q<Button>("last-btn");
|
|
_stopBtn = this.Q<Button>("stop-btn");
|
|
_intervalDropdown = this.Q<DropdownField>("interval-dropdown");
|
|
}
|
|
|
|
private void InitializeEventHandlers()
|
|
{
|
|
_playBtn?.RegisterCallback<ClickEvent>(OnPlayClicked);
|
|
_firstBtn?.RegisterCallback<ClickEvent>(OnFirstClicked);
|
|
_lastBtn?.RegisterCallback<ClickEvent>(OnLastClicked);
|
|
_stopBtn?.RegisterCallback<ClickEvent>(OnStopClicked);
|
|
|
|
// 진행바 드래그/클릭 설정
|
|
if (_progressTrack != null)
|
|
{
|
|
_dragManipulator = new ProgressDragManipulator(this);
|
|
_progressTrack.AddManipulator(_dragManipulator);
|
|
}
|
|
}
|
|
|
|
private void InitializeDropdown()
|
|
{
|
|
if (_intervalDropdown == null) return;
|
|
|
|
var choices = new List<string>();
|
|
for (int i = 0; i <= 10; i++)
|
|
{
|
|
choices.Add($"{i}초");
|
|
}
|
|
_intervalDropdown.choices = choices;
|
|
_intervalDropdown.value = $"{DEFAULT_INTERVAL_SECONDS}초";
|
|
_intervalDropdown.RegisterValueChangedCallback(OnIntervalChanged);
|
|
}
|
|
#endregion
|
|
|
|
#region 공개 메서드 (Public Methods)
|
|
/// <summary>
|
|
/// 시간 범위를 설정합니다.
|
|
/// </summary>
|
|
public void SetTimeRange(DateTime start, DateTime end)
|
|
{
|
|
_startTime = start;
|
|
_endTime = end;
|
|
_currentTime = start;
|
|
|
|
UpdateLabels();
|
|
UpdateProgressBar();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 시간을 설정합니다.
|
|
/// </summary>
|
|
public void SetCurrentTime(DateTime time)
|
|
{
|
|
_currentTime = ClampTime(time);
|
|
UpdateLabels();
|
|
UpdateProgressBar();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 간격 선택 옵션을 설정합니다.
|
|
/// </summary>
|
|
public void SetIntervalChoices(List<int> seconds)
|
|
{
|
|
if (_intervalDropdown == null) return;
|
|
|
|
var choices = new List<string>();
|
|
foreach (var sec in seconds)
|
|
{
|
|
choices.Add($"{sec}초");
|
|
}
|
|
_intervalDropdown.choices = choices;
|
|
_intervalDropdown.index = 0;
|
|
_intervalSeconds = seconds.Count > 0 ? seconds[0] : DEFAULT_INTERVAL_SECONDS;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재생을 시작합니다.
|
|
/// </summary>
|
|
public void Play()
|
|
{
|
|
if (_isPlaying) return;
|
|
_isPlaying = true;
|
|
|
|
UpdatePlayButtonState();
|
|
StartPlayTimer();
|
|
OnPlayStarted?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재생을 일시정지합니다.
|
|
/// </summary>
|
|
public void Pause()
|
|
{
|
|
if (!_isPlaying) return;
|
|
_isPlaying = false;
|
|
|
|
StopPlayTimer();
|
|
UpdatePlayButtonState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 재생을 정지하고 처음으로 돌아갑니다.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
_isPlaying = false;
|
|
_currentTime = _startTime;
|
|
|
|
StopPlayTimer();
|
|
UpdatePlayButtonState();
|
|
UpdateLabels();
|
|
UpdateProgressBar();
|
|
OnPlayStopped?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 리소스를 정리합니다.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
StopPlayTimer();
|
|
UnregisterEventHandlers();
|
|
|
|
OnPlayStarted = null;
|
|
OnPlayStopped = null;
|
|
OnPlayProgress = null;
|
|
OnPositionChanged = null;
|
|
}
|
|
#endregion
|
|
|
|
#region 이벤트 핸들러 (Event Handlers)
|
|
private void OnPlayClicked(ClickEvent evt)
|
|
{
|
|
if (_isPlaying)
|
|
Pause();
|
|
else
|
|
Play();
|
|
}
|
|
|
|
private void OnFirstClicked(ClickEvent evt)
|
|
{
|
|
SetCurrentTime(_startTime);
|
|
OnPositionChanged?.Invoke(_currentTime);
|
|
}
|
|
|
|
private void OnLastClicked(ClickEvent evt)
|
|
{
|
|
SetCurrentTime(_endTime);
|
|
OnPositionChanged?.Invoke(_currentTime);
|
|
}
|
|
|
|
private void OnStopClicked(ClickEvent evt)
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
private void OnIntervalChanged(ChangeEvent<string> evt)
|
|
{
|
|
// "2초" -> 2 파싱
|
|
if (int.TryParse(evt.newValue.Replace("초", ""), out int seconds))
|
|
{
|
|
_intervalSeconds = seconds;
|
|
|
|
// 재생 중이면 타이머 재시작
|
|
if (_isPlaying)
|
|
{
|
|
StopPlayTimer();
|
|
StartPlayTimer();
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region 타이머 (Timer)
|
|
private void StartPlayTimer()
|
|
{
|
|
_playSchedule = schedule.Execute(OnPlayTick).Every(_intervalSeconds * 1000);
|
|
}
|
|
|
|
private void StopPlayTimer()
|
|
{
|
|
_playSchedule?.Pause();
|
|
_playSchedule = null;
|
|
}
|
|
|
|
private void OnPlayTick()
|
|
{
|
|
if (!_isPlaying) return;
|
|
|
|
_currentTime = _currentTime.AddDays(1);
|
|
|
|
if (_currentTime >= _endTime)
|
|
{
|
|
_currentTime = _endTime;
|
|
Pause();
|
|
}
|
|
|
|
UpdateLabels();
|
|
UpdateProgressBar();
|
|
OnPlayProgress?.Invoke(_currentTime);
|
|
}
|
|
#endregion
|
|
|
|
#region UI 업데이트 (UI Updates)
|
|
private void UpdateLabels()
|
|
{
|
|
if (_startLabel != null)
|
|
_startLabel.text = _startTime.ToString("yyyy-MM-dd");
|
|
|
|
if (_endLabel != null)
|
|
_endLabel.text = _endTime.ToString("yyyy-MM-dd");
|
|
|
|
if (_currentTimeLabel != null)
|
|
_currentTimeLabel.text = _currentTime.ToString("yyyy-MM-dd HH:mm");
|
|
}
|
|
|
|
private void UpdateProgressBar()
|
|
{
|
|
if (_progressFill == null) return;
|
|
|
|
float progress = CalculateProgress();
|
|
_progressFill.style.width = Length.Percent(progress * 100);
|
|
}
|
|
|
|
private void UpdatePlayButtonState()
|
|
{
|
|
if (_playBtn == null) return;
|
|
|
|
if (_isPlaying)
|
|
_playBtn.AddToClassList("playing");
|
|
else
|
|
_playBtn.RemoveFromClassList("playing");
|
|
}
|
|
#endregion
|
|
|
|
#region 유틸리티 (Utilities)
|
|
private float CalculateProgress()
|
|
{
|
|
double total = (_endTime - _startTime).TotalDays;
|
|
if (total <= 0) return 0;
|
|
|
|
double current = (_currentTime - _startTime).TotalDays;
|
|
return Mathf.Clamp01((float)(current / total));
|
|
}
|
|
|
|
private DateTime ClampTime(DateTime time)
|
|
{
|
|
if (time < _startTime) return _startTime;
|
|
if (time > _endTime) return _endTime;
|
|
return time;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 정규화된 위치(0~1)로부터 시간을 설정합니다. (드래그용)
|
|
/// </summary>
|
|
internal void SetProgressFromPosition(float normalizedPosition)
|
|
{
|
|
double totalDays = (_endTime - _startTime).TotalDays;
|
|
double targetDays = totalDays * normalizedPosition;
|
|
|
|
_currentTime = _startTime.AddDays(targetDays);
|
|
_currentTime = ClampTime(_currentTime);
|
|
|
|
UpdateLabels();
|
|
UpdateProgressBar();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 드래그 완료 시 이벤트를 발생시킵니다.
|
|
/// </summary>
|
|
internal void NotifyPositionChanged()
|
|
{
|
|
OnPositionChanged?.Invoke(_currentTime);
|
|
}
|
|
|
|
internal float GetTrackWidth()
|
|
{
|
|
return _progressTrack?.layout.width ?? 0;
|
|
}
|
|
#endregion
|
|
|
|
#region 이벤트 해제 (Unregister)
|
|
private void UnregisterEventHandlers()
|
|
{
|
|
_playBtn?.UnregisterCallback<ClickEvent>(OnPlayClicked);
|
|
_firstBtn?.UnregisterCallback<ClickEvent>(OnFirstClicked);
|
|
_lastBtn?.UnregisterCallback<ClickEvent>(OnLastClicked);
|
|
_stopBtn?.UnregisterCallback<ClickEvent>(OnStopClicked);
|
|
|
|
if (_progressTrack != null && _dragManipulator != null)
|
|
{
|
|
_progressTrack.RemoveManipulator(_dragManipulator);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// 진행바의 드래그/클릭을 처리하는 Manipulator입니다.
|
|
/// </summary>
|
|
public class ProgressDragManipulator : PointerManipulator
|
|
{
|
|
private readonly PlayBar _playBar;
|
|
private bool _isActive;
|
|
|
|
public ProgressDragManipulator(PlayBar playBar)
|
|
{
|
|
_playBar = playBar;
|
|
activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
|
|
}
|
|
|
|
protected override void RegisterCallbacksOnTarget()
|
|
{
|
|
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
|
|
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
|
|
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
|
|
target.RegisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
|
|
}
|
|
|
|
protected override void UnregisterCallbacksFromTarget()
|
|
{
|
|
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
|
|
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
|
|
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
|
|
target.UnregisterCallback<PointerCaptureOutEvent>(OnPointerCaptureOut);
|
|
}
|
|
|
|
private void OnPointerDown(PointerDownEvent evt)
|
|
{
|
|
if (!CanStartManipulation(evt)) return;
|
|
|
|
_isActive = true;
|
|
target.CapturePointer(evt.pointerId);
|
|
UpdatePosition(evt.localPosition);
|
|
evt.StopPropagation();
|
|
}
|
|
|
|
private void OnPointerMove(PointerMoveEvent evt)
|
|
{
|
|
if (!_isActive || !target.HasPointerCapture(evt.pointerId)) return;
|
|
|
|
UpdatePosition(evt.localPosition);
|
|
}
|
|
|
|
private void OnPointerUp(PointerUpEvent evt)
|
|
{
|
|
if (!_isActive) return;
|
|
|
|
_isActive = false;
|
|
target.ReleasePointer(evt.pointerId);
|
|
|
|
// 드래그 종료 시에만 이벤트 발생
|
|
_playBar.NotifyPositionChanged();
|
|
evt.StopPropagation();
|
|
}
|
|
|
|
private void OnPointerCaptureOut(PointerCaptureOutEvent evt)
|
|
{
|
|
_isActive = false;
|
|
}
|
|
|
|
private void UpdatePosition(Vector2 localPosition)
|
|
{
|
|
float trackWidth = _playBar.GetTrackWidth();
|
|
if (trackWidth <= 0) return;
|
|
|
|
float normalizedPosition = Mathf.Clamp01(localPosition.x / trackWidth);
|
|
_playBar.SetProgressFromPosition(normalizedPosition);
|
|
}
|
|
}
|
|
}
|