Editor/HumanoidMocap/VideoDropArea.cs
using System;
using System.IO;
using Editor;
using Sandbox;
namespace HumanoidMocapX.EditorTools;
// First-load surface, styled after Humanoid Rigger's ModelDropArea: a dark-gray
// rounded box on the gray window, centered prompt and a green choose button.
sealed class VideoDropArea : Widget
{
readonly Action<string> import;
readonly VideoIcon icon;
readonly Label title, help, or;
readonly Button choose;
readonly string prompt;
int hover;
public VideoDropArea(Widget parent, Action<string> import, Action chooseVideo) : base(parent)
{
this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;
Layout.AddStretchCell();
var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;
icon=center.Add(new VideoIcon(this));
title=center.Add(new Label.Subtitle("Video to motion"){Alignment=TextFlag.Center});
prompt="Drag and drop a video here (.mp4, .mov, .m4v, .avi, .mkv, .webm)";
help=center.Add(new Label(prompt,this){Alignment=TextFlag.Center});
help.SetStyles($"color: {Theme.TextLight.Hex};");
or=center.Add(new Label("or",this){Alignment=TextFlag.Center});
var choice=center.AddRow();choice.Spacing=8;choice.AddStretchCell();
choose=choice.Add(new Button.Primary("Choose Video"){Icon="video_file",Tint=Theme.Green,MinimumWidth=140,FixedHeight=32,Clicked=chooseVideo});
choice.AddStretchCell();
row.AddStretchCell();Layout.AddStretchCell();
}
/// <summary>Grays the area out and refuses drops: a blocked icon, "Currently disabled" and
/// <paramref name="reason"/> replace the prompt and buttons, until called again with null.</summary>
public void SetLocked(string reason)
{
var locked=reason is not null;
Enabled=!locked;AcceptDrops=!locked;hover=0;
icon.Icon=locked?"block":"video_file";
title.Text=locked?"Currently disabled":"Video to motion";
help.Text=reason??prompt;help.SetStyles($"color: {(locked?Theme.Yellow:Theme.TextLight).Hex};");
or.Visible=choose.Visible=!locked;
Update();
}
bool Supported(string path)=>Path.GetExtension(path).ToLowerInvariant() switch {
".mp4" or ".mov" or ".m4v" or ".avi" or ".mkv" or ".webm" => true,
_ => false
};
// Copy is the one action every file manager offers; an X11 drag source (Dolphin through
// XWayland) can refuse a drop that asks for Link, so the file never arrives.
public override void OnDragHover(DragEvent e)
{bool valid=DroppedFile(e.Data) is { } path&&Supported(path);hover=valid?1:-1;if(valid)e.Action=DropAction.Copy;Update();}
public override void OnDragDrop(DragEvent e)
{hover=0;if(DroppedFile(e.Data) is { } path&&Supported(path)){e.Action=DropAction.Copy;import(path);}Update();}
/// <summary>The first dropped local file, read from its file URL. s&box's text fallback
/// strips the leading '/' from Linux paths. Null when the drag carries no local file.</summary>
static string DroppedFile(DragData data)
{
if(data.Url is { IsFile: true } url)return url.LocalPath;
// A '#' in a file name would otherwise start a URI fragment.
var first=data.Text?.Split('\n')[0].Trim().Replace("#","%23");
return Uri.TryCreate(first,UriKind.Absolute,out var uri)&&uri.IsFile?uri.LocalPath:null;
}
public override void OnDragLeave(){hover=0;Update();}
protected override void OnPaint()
{
Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);
Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Enabled&&Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);
Paint.DrawRect(LocalRect.Shrink(1),4);
}
sealed class VideoIcon : Widget
{
string icon="video_file";
public VideoIcon(Widget parent):base(parent){FixedHeight=48;}
public string Icon{get=>icon;set{icon=value;Update();}}
protected override void OnPaint(){Paint.SetPen(Theme.TextLight);Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),icon,40);}
}
}