Files
XRLib/Assets/Scripts/SHI/modal/PlayBar.cs

473 lines
14 KiB
C#
Raw Normal View History

#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);
}
}
}