425 lines
14 KiB
C#
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
|
|
}
|
|
|
|
|
|
}
|
|
|
|
}
|