JD2022-TU1/main/tools/framework/JD.Timeline/Dance/PictogramBitmapHandler.cs

425 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Forms;
using JD.Application.Utils;
using JD.Timeline.Model.Clips;
using JD.Timeline.View.VideoView;
using JetBrains.Annotations;
using TeaBox;
using JD.Core.Drawing;
using JD.Serialization;
using PropertyChangedEventArgs = System.ComponentModel.PropertyChangedEventArgs;
using Size = System.Drawing.Size;
namespace JD.Timeline.Dance
{
public class PictogramBitmapHandler : INotifyPropertyChanged
{
#region Variables
private static readonly Bitmap _placeHolder1 = Properties.Resources.Icon_512x512_PictogramPlaceholder_1;
private static readonly Bitmap _placeHolder2 = Properties.Resources.Icon_512x512_PictogramPlaceholder_2;
private static readonly Bitmap _placeHolder3 = Properties.Resources.Icon_512x512_PictogramPlaceholder_3;
private static readonly Bitmap _placeHolder4 = Properties.Resources.Icon_512x512_PictogramPlaceholder_4;
private static readonly Bitmap _placeHolder6 = Properties.Resources.Icon_512x512_PictogramPlaceholder_6;
private static readonly Bitmap _errorImageMissing = Properties.Resources.Icon_20x20_imagemissing;
private Bitmap _bitmap;
private Bitmap _bitmapThumbnail;
private bool _isDirty;
private readonly PictogramBitmapWatcher _pictogramBitmapWatcher;
#endregion
#region Constructor
public PictogramBitmapHandler(PictogramModel model, PictogramBitmapWatcher pictogramBitmapWatcher)
{
TempFilesUsedForEdit = new List<string>();
CurrentState = State.Idle;
_pictogramBitmapWatcher = pictogramBitmapWatcher;
Model = model;
//If a pictogram exists on disk, we load it
if (System.IO.File.Exists(model.PictoFullPath()))
{
Load(model.PictoFullPath());
}
//otherwise we create a new pictogram from placeholder
else
{
LoadPlaceholder();
}
}
#endregion
#region Properties
/// <summary>
/// The pictogram bitmap in its full resolution
/// </summary>
[CanBeNull]
public Bitmap Bitmap
{
get
{
return _bitmap;
}
private set
{
if (_bitmap != value && (CurrentState == State.Idle || CurrentState == State.Loading))
{
CurrentState = State.Processing;
_bitmap = value;
ThreadPool.QueueUserWorkItem(state =>
{
Rectangle rect;
Bitmap thumbnail;
lock (_bitmap)
{
rect = ImageUtility.ComputeAlphaBorderlessBounds(_bitmap, 200);
thumbnail = ImageUtility.ResizeImage(_bitmap, 64);
}
TeaBox.ObjectModel.Application.Main.Dispatcher.DispatchAsync(() =>
{
PictoAlphalessRect = rect;
BitmapThumbnail = thumbnail;
HasAlpha = thumbnail != null && thumbnail.GetPixel(0, 0).A != 255;
OnPropertyChanged("Bitmap");
Model.NotifyPropertyChanged("Bitmap");
UpdateModelDuration();
CurrentState = State.Idle;
});
});
}
}
}
/// <summary>
/// The pictogram bitmap in thumbnail resolution
/// </summary>
public Bitmap BitmapThumbnail
{
get
{
return _bitmapThumbnail;
}
private set
{
if (_bitmapThumbnail != value)
{
_bitmapThumbnail = value;
OnPropertyChanged("BitmapThumbnail");
Model.NotifyPropertyChanged("BitmapThumbnail");
}
}
}
/// <summary>
/// The pictogram model
/// </summary>
public PictogramModel Model
{
get; set;
}
/// <summary>
/// The bounds of pictogram when you remove all transparent areas
/// </summary>
public Rectangle PictoAlphalessRect
{
get; set;
}
/// <summary>
/// The pictogram file dimensions according to the number of players
/// </summary>
public Size PictoExpectedSize
{
get
{
switch (Model.Tape.DancePlayersCount)
{
case 1: return new Size(512, 512);
case 2: return new Size(740, 512);
case 3: return new Size(740, 512);
case 4: return new Size(740, 512);
case 6: return new Size(968, 512);
default: return new Size(512, 512);
}
}
}
/// <summary>
/// Indicates whether there are some unsaved modifications
/// </summary>
public bool IsDirty
{
get { return _isDirty; }
set
{
_isDirty = value;
if (_isDirty)
{
TeaBox.ObjectModel.Application.Main.Dispatcher.DispatchAsync(() => Model.NotifyPropertyChanged("BitmapIsDirty"));
}
}
}
/// <summary>
/// Indicates whether pictogram has alpha
/// </summary>
private bool HasAlpha { get; set; }
/// <summary>
/// The current state
/// </summary>
public State CurrentState { get; set; }
/// <summary>
/// All temporary files generated when clicking on Edit
/// </summary>
public List<string> TempFilesUsedForEdit { get; private set; }
#endregion
#region Methods
/// <summary>
/// Reloads the pictogram from disk in a specific location
/// </summary>
/// <param name="filePath">the file path</param>
/// <param name="delayInMs">a delay before trying to load file</param>
public void Load(string filePath, int delayInMs = 0)
{
if (CurrentState != State.Idle)
return;
CurrentState = State.Loading;
ThreadPool.QueueUserWorkItem(state =>
{
if (delayInMs > 0)
{
Thread.Sleep(delayInMs);
}
Bitmap bitmap;
try
{
using (var bound = Image.FromFile(filePath))
{
bitmap = new Bitmap(bound);
}
}
catch
{
//If there is an error in loading of image, we assign an error image
lock (_errorImageMissing)
{
bitmap = (Bitmap)_errorImageMissing.Clone();
}
}
TeaBox.ObjectModel.Application.Main.Dispatcher.DispatchAsync(() =>
{
Bitmap = bitmap;
IsDirty = (filePath != Model.PictoFullPath());
CurrentState = State.Idle;
});
});
}
/// <summary>
/// Load pictogram from placeholder
/// </summary>
public void LoadPlaceholder()
{
if (CurrentState != State.Idle)
return;
CurrentState = State.Loading;
ThreadPool.QueueUserWorkItem(state =>
{
Bitmap bitmap;
var placeholder = GetPlaceholder(Model.Tape.DancePlayersCount);
lock (placeholder)
{
bitmap = (Bitmap)placeholder.Clone();
}
TeaBox.ObjectModel.Application.Main.Dispatcher.DispatchAsync(() =>
{
Bitmap = bitmap;
IsDirty = true;
CurrentState = State.Idle;
});
});
}
/// <summary>
/// Saves the pictogram on its default location
/// </summary>
public void Save()
{
if (Model.PictoPath == null || string.IsNullOrEmpty(Model.PictoPath.ToString()))
return;
Save(Model.PictoFullPath());
IsDirty = false;
}
/// <summary>
/// Saves the pictogram on a specific location
/// </summary>
public void Save(string filePath)
{
if (CurrentState != State.Idle)
return;
try
{
CurrentState = State.Saving;
var fileInfo = new System.IO.FileInfo(filePath);
if (fileInfo.Exists && fileInfo.IsReadOnly)
{
return;
}
//We temporary disable the file watcher and save the file
_pictogramBitmapWatcher.FileWatcher.EnableRaisingEvents = false;
Bitmap.Save(filePath, ImageFormat.Png);
}
finally
{
_pictogramBitmapWatcher.FileWatcher.EnableRaisingEvents = true;
CurrentState = State.Idle;
}
}
/// <summary>
/// Take a snapshot from the current video and use it as placeholder
/// </summary>
public void TakeSnapshotFromVideo()
{
if (CurrentState != State.Idle)
return;
var videoPad = TeaBox.ObjectModel.Application.Main.Pads.Select(x => x.Content).OfType<TapeVideoPad>().FirstOrDefault();
if (videoPad == null)
{
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(5), "Can't find video", "Can't find video", ToolTipIcon.Error);
return;
}
if (videoPad.VideoControl == null)
{
MessageService.MessageService.ShowBalloon(TimeSpan.FromSeconds(5), "Can't take snapshot", "This feature is only available in offline mode for the moment. Please work offline.", ToolTipIcon.Warning);
return;
}
var bitmap = videoPad.VideoControl.Snapshot();
bitmap = ImageUtility.ComputeAlphaBorderlessImage(bitmap, 200);
bitmap = ImageUtility.ComputeAlphalessImage(bitmap, 200);
if (bitmap.GetPixel(0, 0).A != 255)
{
bitmap = ImageUtility.AddBorder(bitmap, new Thickness(0, bitmap.Height * 0.35, 0, 0), Color.Transparent);
}
IsDirty = true;
Bitmap = ImageUtility.FitBitmapKeepingRatio(bitmap, PictoExpectedSize);
}
/// <summary>
/// Allow to edit a pictogram in an external program like Paint.Net/Gimp...
/// </summary>
public void EditPictogram()
{
var randomFileName = $"{Model.Name}({System.IO.Path.GetRandomFileName()}).png";
string tempPictogram = System.IO.Path.Combine(PictogramBitmapWatcher.TempDirPath, randomFileName);
Save(tempPictogram);
if (System.IO.File.Exists(tempPictogram))
{
TempFilesUsedForEdit.Add(randomFileName);
System.Diagnostics.Process.Start(tempPictogram);
}
}
/// <summary>
/// Computes the duration of the pictogram, representing the scrolling duration of pictogram ingame
/// </summary>
private void UpdateModelDuration()
{
if (Model == null || Model.Tape == null)
return;
int alphaLessPictogramWidthInPixels = PictoAlphalessRect.Width;
if (!HasAlpha)
{
switch (Model.Tape.DancePlayersCount)
{
case 1: alphaLessPictogramWidthInPixels = 264; break; //based on Starships mean value
case 2: alphaLessPictogramWidthInPixels = 428; break; //based on Get Lucky mean value
case 3: alphaLessPictogramWidthInPixels = 594; break; //based on GimmeGimmeOSC mean value
case 4: alphaLessPictogramWidthInPixels = 673; break; //based on BlameIt mean value
case 6: alphaLessPictogramWidthInPixels = 751; break; // based on KissYouSIX
}
}
Model.BeatDuration = (float)(PictogramScrollingUtils.GetBeatsPerPixel(Model.Tape.DancePlayersCount, 0,
Model.Tape.TryGetWave().UnitConverter) * alphaLessPictogramWidthInPixels);
}
/// <summary>
/// Gets the placeholder image according to the count of players
/// </summary>
public Bitmap GetPlaceholder(int dancePlayersCount)
{
switch (dancePlayersCount)
{
case 1: return _placeHolder1;
case 2: return _placeHolder2;
case 3: return _placeHolder3;
case 4: return _placeHolder4;
case 6: return _placeHolder6;
default: return _placeHolder1;
}
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
public enum State
{
Idle,
Loading,
Processing,
Saving
}
}
}