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(); 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 /// /// The pictogram bitmap in its full resolution /// [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; }); }); } } } /// /// The pictogram bitmap in thumbnail resolution /// public Bitmap BitmapThumbnail { get { return _bitmapThumbnail; } private set { if (_bitmapThumbnail != value) { _bitmapThumbnail = value; OnPropertyChanged("BitmapThumbnail"); Model.NotifyPropertyChanged("BitmapThumbnail"); } } } /// /// The pictogram model /// public PictogramModel Model { get; set; } /// /// The bounds of pictogram when you remove all transparent areas /// public Rectangle PictoAlphalessRect { get; set; } /// /// The pictogram file dimensions according to the number of players /// 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); } } } /// /// Indicates whether there are some unsaved modifications /// public bool IsDirty { get { return _isDirty; } set { _isDirty = value; if (_isDirty) { TeaBox.ObjectModel.Application.Main.Dispatcher.DispatchAsync(() => Model.NotifyPropertyChanged("BitmapIsDirty")); } } } /// /// Indicates whether pictogram has alpha /// private bool HasAlpha { get; set; } /// /// The current state /// public State CurrentState { get; set; } /// /// All temporary files generated when clicking on Edit /// public List TempFilesUsedForEdit { get; private set; } #endregion #region Methods /// /// Reloads the pictogram from disk in a specific location /// /// the file path /// a delay before trying to load file 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; }); }); } /// /// Load pictogram from placeholder /// 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; }); }); } /// /// Saves the pictogram on its default location /// public void Save() { if (Model.PictoPath == null || string.IsNullOrEmpty(Model.PictoPath.ToString())) return; Save(Model.PictoFullPath()); IsDirty = false; } /// /// Saves the pictogram on a specific location /// 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; } } /// /// Take a snapshot from the current video and use it as placeholder /// public void TakeSnapshotFromVideo() { if (CurrentState != State.Idle) return; var videoPad = TeaBox.ObjectModel.Application.Main.Pads.Select(x => x.Content).OfType().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); } /// /// Allow to edit a pictogram in an external program like Paint.Net/Gimp... /// 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); } } /// /// Computes the duration of the pictogram, representing the scrolling duration of pictogram ingame /// 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); } /// /// Gets the placeholder image according to the count of players /// 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 } } }