diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 38ee4e0b..88949276 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,10 +30,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} + queries: +security-extended - name: Autobuild run: | diff --git a/src/COM/DesktopWallpaper.cs b/src/COM/DesktopWallpaper.cs index bdd042f1..4e1edb8a 100644 --- a/src/COM/DesktopWallpaper.cs +++ b/src/COM/DesktopWallpaper.cs @@ -16,6 +16,15 @@ public struct COLORREF public byte B; } + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + public enum DesktopSlideshowOptions { DSO_SHUFFLEIMAGES = 0x01, @@ -59,7 +68,7 @@ public interface IDesktopWallpaper uint GetMonitorDevicePathCount(); - System.Windows.Rect GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID); + RECT GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID); void SetBackgroundColor([MarshalAs(UnmanagedType.U4)] COLORREF color); diff --git a/src/Skia/ImageCache.cs b/src/Skia/ImageCache.cs new file mode 100644 index 00000000..ef45058f --- /dev/null +++ b/src/Skia/ImageCache.cs @@ -0,0 +1,160 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; +using SkiaSharp; + +namespace WinDynamicDesktop.Skia +{ + sealed class ImageCache + { + readonly int maxWidth; + readonly int maxHeight; + readonly object cacheLock = new object(); + readonly Dictionary images = new Dictionary(); + + public SKImage this[Uri uri] + { + get + { + lock (cacheLock) + { + if (images.TryGetValue(uri, out var image)) + { + return image; + } + + var img = CreateImage(uri); + if (img != null) + { + images.Add(uri, img); + } + return img; + } + } + } + + public void Clear() + { + lock (cacheLock) + { + foreach (var image in images.Values) + { + image?.Dispose(); + } + images.Clear(); + } + GC.Collect(); + } + + public ImageCache(bool limitDecodeSize = true) + { + if (limitDecodeSize) + { + int maxArea = 0; + foreach (Screen screen in Screen.AllScreens) + { + int area = screen.Bounds.Width * screen.Bounds.Height; + if (area > maxArea) + { + maxArea = area; + maxWidth = screen.Bounds.Width; + maxHeight = screen.Bounds.Height; + } + } + } + else + { + maxWidth = int.MaxValue; + maxHeight = int.MaxValue; + } + } + + private SKImage CreateImage(Uri uri) + { + try + { + Stream stream = null; + + if (uri.IsAbsoluteUri && uri.Scheme == "file") + { + string path = uri.LocalPath; + if (File.Exists(path)) + { + stream = File.OpenRead(path); + } + } + else if (!uri.IsAbsoluteUri) + { + // Embedded resource + stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(uri.OriginalString); + } + + if (stream == null) + { + return null; + } + + using (stream) + { + using (var codec = SKCodec.Create(stream)) + { + if (codec == null) + { + return null; + } + + var info = codec.Info; + + // Calculate target dimensions + int targetWidth = info.Width; + int targetHeight = info.Height; + + if (info.Width > maxWidth || info.Height > maxHeight) + { + float scale = Math.Min((float)maxWidth / info.Width, (float)maxHeight / info.Height); + targetWidth = (int)(info.Width * scale); + targetHeight = (int)(info.Height * scale); + } + + // Decode at native size + using (var sourceBitmap = new SKBitmap(info)) + { + if (codec.GetPixels(sourceBitmap.Info, sourceBitmap.GetPixels()) != SKCodecResult.Success) + { + return null; + } + + // If scaling is needed, create scaled version with high quality + if (targetWidth != sourceBitmap.Width || targetHeight != sourceBitmap.Height) + { + using (var scaledBitmap = new SKBitmap(targetWidth, targetHeight, info.ColorType, info.AlphaType)) + { + sourceBitmap.ScalePixels(scaledBitmap, new SKSamplingOptions(SKCubicResampler.Mitchell)); + var image = SKImage.FromBitmap(scaledBitmap); + return image; + } + } + else + { + // No scaling needed + var image = SKImage.FromBitmap(sourceBitmap); + return image; + } + } + } + } + } + catch + { + return null; + } + } + } +} diff --git a/src/Skia/ThemePreviewRenderer.cs b/src/Skia/ThemePreviewRenderer.cs new file mode 100644 index 00000000..f4aae7a5 --- /dev/null +++ b/src/Skia/ThemePreviewRenderer.cs @@ -0,0 +1,248 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using SkiaSharp; +using System; +using System.Drawing; + +namespace WinDynamicDesktop.Skia +{ + internal class ThemePreviewRenderer + { + private const int MARGIN_STANDARD = 20; + private const int BORDER_RADIUS = 5; + private const int ARROW_AREA_WIDTH = 80; + private const byte OVERLAY_ALPHA = 127; + private const float OPACITY_NORMAL = 0.5f; + private const float OPACITY_HOVER = 1.0f; + + // FontAwesome icons + private const string ICON_PLAY = "\uf04b"; + private const string ICON_PAUSE = "\uf04c"; + private const string ICON_CHEVRON_LEFT = "\uf053"; + private const string ICON_CHEVRON_RIGHT = "\uf054"; + + private readonly SKPaint basePaint; + private readonly SKTypeface systemFont; + private readonly SKTypeface systemFontBold; + private readonly SKFont titleFont; + private readonly SKFont textFont; + private readonly SKFont iconFont16; + private readonly SKFont iconFont20; + private readonly SKColor overlayColor; + private readonly SKSamplingOptions samplingOptions; + + // Hit test regions (updated during rendering) + public Rectangle TitleBoxRect { get; private set; } + public Rectangle PlayButtonRect { get; private set; } + public Rectangle DownloadSizeLabelRect { get; private set; } + public Rectangle AuthorLabelRect { get; private set; } + public Rectangle DownloadMessageRect { get; private set; } + public Rectangle LeftArrowRect { get; private set; } + public Rectangle RightArrowRect { get; private set; } + public Rectangle[] CarouselIndicatorRects { get; private set; } + + private enum Side { Left, Right } + + public ThemePreviewRenderer(SKTypeface fontAwesome, string systemFontName) + { + basePaint = new SKPaint { IsAntialias = true }; + systemFont = SKTypeface.FromFamilyName(systemFontName); + systemFontBold = SKTypeface.FromFamilyName(systemFontName, SKFontStyle.Bold); + titleFont = new SKFont(systemFontBold, 19); + textFont = new SKFont(systemFont, 16); + iconFont16 = new SKFont(fontAwesome, 16); + iconFont20 = new SKFont(fontAwesome, 20); + overlayColor = new SKColor(0, 0, 0, OVERLAY_ALPHA); + samplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); + } + + public void DrawImage(SKCanvas canvas, SKImage image, SKImageInfo info, float opacity) + { + var destRect = new SKRect(0, 0, info.Width, info.Height); + + if (opacity >= 1.0f) + { + // Fast path for fully opaque images + canvas.DrawImage(image, destRect, samplingOptions, null); + } + else + { + // Apply opacity with color filter + using (var paint = new SKPaint()) + using (var colorFilter = SKColorFilter.CreateBlendMode( + SKColors.White.WithAlpha((byte)(255 * opacity)), + SKBlendMode.DstIn)) + { + paint.IsAntialias = true; + paint.ColorFilter = colorFilter; + canvas.DrawImage(image, destRect, samplingOptions, paint); + } + } + } + + public void DrawOverlay(SKCanvas canvas, SKImageInfo info, ThemePreviewerViewModel viewModel, + ThemePreviewer.HoveredItem hoveredItem) + { + // Set arrow hit regions + LeftArrowRect = new Rectangle(0, 0, ARROW_AREA_WIDTH, info.Height); + RightArrowRect = new Rectangle(info.Width - ARROW_AREA_WIDTH, 0, ARROW_AREA_WIDTH, info.Height); + + // Draw left and right arrow button areas + DrawArrowArea(canvas, info, Side.Left, hoveredItem == ThemePreviewer.HoveredItem.LeftArrow); + DrawArrowArea(canvas, info, Side.Right, hoveredItem == ThemePreviewer.HoveredItem.RightArrow); + + // Title and preview text box (top left) + SKRect titleBounds; + titleFont.MeasureText(viewModel.Title ?? "", out titleBounds); + SKRect previewBounds; + textFont.MeasureText(viewModel.PreviewText ?? "", out previewBounds); + + float boxWidth = Math.Max(titleBounds.Width, previewBounds.Width) + MARGIN_STANDARD; + float boxHeight = 19 + 4 + 16 + MARGIN_STANDARD; + TitleBoxRect = new Rectangle(MARGIN_STANDARD, MARGIN_STANDARD, (int)boxWidth, (int)boxHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(TitleBoxRect.X, TitleBoxRect.Y, TitleBoxRect.Width, TitleBoxRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + basePaint.Color = SKColors.White; + canvas.DrawText(viewModel.Title ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19, titleFont, basePaint); + canvas.DrawText(viewModel.PreviewText ?? "", TitleBoxRect.X + 10, TitleBoxRect.Y + 8 + 19 + 5 + 16, textFont, basePaint); + + // Play/Pause button (top right) + int playButtonSize = 40; + PlayButtonRect = new Rectangle(info.Width - playButtonSize - MARGIN_STANDARD, MARGIN_STANDARD, playButtonSize, playButtonSize); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(PlayButtonRect.X, PlayButtonRect.Y, PlayButtonRect.Width, PlayButtonRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + float playOpacity = hoveredItem == ThemePreviewer.HoveredItem.PlayButton ? OPACITY_HOVER : OPACITY_NORMAL; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * playOpacity)); + string playIcon = viewModel.IsPlaying ? ICON_PAUSE : ICON_PLAY; + SKRect textBounds; + iconFont16.MeasureText(playIcon, out textBounds); + float centerX = PlayButtonRect.X + PlayButtonRect.Width / 2; + float centerY = PlayButtonRect.Y + PlayButtonRect.Height / 2; + canvas.DrawText(playIcon, centerX - textBounds.MidX, centerY - textBounds.MidY, iconFont16, basePaint); + + // Corner labels + DrawCornerLabel(canvas, info, viewModel.DownloadSize, Side.Left, out var downloadSizeRect); + DownloadSizeLabelRect = downloadSizeRect; + DrawCornerLabel(canvas, info, viewModel.Author, Side.Right, out var authorRect); + AuthorLabelRect = authorRect; + + // Download message (centered bottom) + if (!string.IsNullOrEmpty(viewModel.Message)) + { + SKRect msgBounds; + textFont.MeasureText(viewModel.Message, out msgBounds); + float msgWidth = msgBounds.Width + 16; + float msgHeight = 6 + 16 + 6; + DownloadMessageRect = new Rectangle((int)(info.Width / 2 - msgWidth / 2), info.Height - (int)msgHeight - 15, (int)msgWidth, (int)msgHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(DownloadMessageRect.X, DownloadMessageRect.Y, DownloadMessageRect.Width, DownloadMessageRect.Height), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + float msgOpacity = hoveredItem == ThemePreviewer.HoveredItem.DownloadButton ? OPACITY_HOVER : OPACITY_NORMAL; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * msgOpacity)); + canvas.DrawText(viewModel.Message, DownloadMessageRect.X + 8, DownloadMessageRect.Y + 5 + 16, textFont, basePaint); + } + else + { + DownloadMessageRect = Rectangle.Empty; + } + + // Carousel indicators + if (viewModel.CarouselIndicatorsVisible && viewModel.Items.Count > 0) + { + DrawCarouselIndicators(canvas, info, viewModel.Items.Count, viewModel.SelectedIndex); + } + else + { + CarouselIndicatorRects = null; + } + } + + private void DrawArrowArea(SKCanvas canvas, SKImageInfo info, Side side, bool isHovered) + { + float opacity = isHovered ? OPACITY_HOVER : OPACITY_NORMAL; + + float x = side == Side.Left ? 40 : info.Width - 40; + float y = info.Height / 2; + + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + + string icon = side == Side.Left ? ICON_CHEVRON_LEFT : ICON_CHEVRON_RIGHT; + SKRect textBounds; + iconFont20.MeasureText(icon, out textBounds); + canvas.DrawText(icon, x - textBounds.MidX, y - textBounds.MidY, iconFont20, basePaint); + } + + private void DrawCornerLabel(SKCanvas canvas, SKImageInfo info, string text, Side side, out Rectangle labelRect) + { + if (string.IsNullOrEmpty(text)) + { + labelRect = Rectangle.Empty; + return; + } + + SKRect textBounds; + textFont.MeasureText(text, out textBounds); + + var padding = new System.Windows.Forms.Padding(8, 4, 10, 10); // Left, Top, Right, Bottom + float leftMargin = side == Side.Right ? padding.Left : padding.Right; + float rightMargin = side == Side.Right ? padding.Right : padding.Left; + float borderWidth = textBounds.Width + leftMargin + rightMargin; + float borderHeight = padding.Top + 16 + padding.Bottom; + + int offset = 3; + float rectX = side == Side.Right ? info.Width - borderWidth + offset : -offset; + float rectY = info.Height - borderHeight + offset; + labelRect = new Rectangle((int)rectX, (int)rectY, (int)borderWidth, (int)borderHeight); + + basePaint.Color = overlayColor; + canvas.DrawRoundRect(SKRect.Create(rectX, rectY, borderWidth, borderHeight), BORDER_RADIUS, BORDER_RADIUS, basePaint); + + basePaint.Color = SKColors.White.WithAlpha(OVERLAY_ALPHA); + float textX = side == Side.Right ? info.Width - textBounds.Width - rightMargin + offset : leftMargin - offset; + float textY = rectY + padding.Top + 16; + canvas.DrawText(text, textX, textY, textFont, basePaint); + } + + private void DrawCarouselIndicators(SKCanvas canvas, SKImageInfo info, int count, int selectedIndex) + { + int indicatorWidth = 30; + int indicatorHeight = 3; + int itemSpacing = 6; + int totalWidth = count * indicatorWidth + (count - 1) * itemSpacing; + int startX = (info.Width - totalWidth) / 2; + int y = info.Height - 16 - 16; // bottom margin - half of clickable height + + CarouselIndicatorRects = new Rectangle[count]; + + for (int i = 0; i < count; i++) + { + float opacity = (i == selectedIndex) ? OPACITY_HOVER : OPACITY_NORMAL; + basePaint.Color = SKColors.White.WithAlpha((byte)(255 * opacity)); + + int rectX = startX + i * (indicatorWidth + itemSpacing); + canvas.DrawRect(rectX, y - indicatorHeight / 2, indicatorWidth, indicatorHeight, basePaint); + + // Cache full clickable area for hit testing + CarouselIndicatorRects[i] = new Rectangle(rectX, y - 16, indicatorWidth, 32); + } + } + + public void Dispose() + { + basePaint?.Dispose(); + systemFont?.Dispose(); + systemFontBold?.Dispose(); + titleFont?.Dispose(); + textFont?.Dispose(); + iconFont16?.Dispose(); + iconFont20?.Dispose(); + } + } +} diff --git a/src/Skia/ThemePreviewer.cs b/src/Skia/ThemePreviewer.cs new file mode 100644 index 00000000..14f33bb0 --- /dev/null +++ b/src/Skia/ThemePreviewer.cs @@ -0,0 +1,265 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using SkiaSharp; +using SkiaSharp.Views.Desktop; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; + +namespace WinDynamicDesktop.Skia +{ + public class ThemePreviewer : SKControl + { + private const int ANIMATION_DURATION_MS = 600; + private const int ANIMATION_FPS = 120; + + public ThemePreviewerViewModel ViewModel { get; } + + private readonly Timer fadeTimer; + private float fadeProgress = 0f; + private bool isAnimating = false; + private DateTime animationStartTime; + + private static SKTypeface fontAwesome; + private readonly ThemePreviewRenderer renderer; + private HoveredItem hoveredItem = HoveredItem.None; + + public enum HoveredItem + { + None, + PlayButton, + LeftArrow, + RightArrow, + DownloadButton + } + + public ThemePreviewer() + { + ViewModel = new ThemePreviewerViewModel(StartAnimation, StopAnimation); + DoubleBuffered = true; + + // Load FontAwesome font once + if (fontAwesome == null) + { + using (Stream fontStream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("WinDynamicDesktop.resources.fonts.fontawesome-webfont.ttf")) + { + fontAwesome = SKTypeface.FromStream(fontStream); + } + } + + renderer = new ThemePreviewRenderer(fontAwesome, Control.DefaultFont.FontFamily.Name); + + // Timer for smooth fade animations + fadeTimer = new Timer + { + Interval = 1000 / ANIMATION_FPS + }; + fadeTimer.Tick += FadeTimer_Tick; + + MouseEnter += (s, e) => ViewModel.IsMouseOver = true; + MouseLeave += OnMouseLeave; + + ViewModel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(ThemePreviewerViewModel.BackImage) || + e.PropertyName == nameof(ThemePreviewerViewModel.FrontImage) || + e.PropertyName == nameof(ThemePreviewerViewModel.IsPlaying) || + e.PropertyName == nameof(ThemePreviewerViewModel.DownloadSize)) + { + Invalidate(); + } + }; + + KeyDown += ThemePreviewer_KeyDown; + } + + private void ThemePreviewer_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Left) + { + ViewModel.Previous(); + e.Handled = true; + } + else if (e.KeyCode == Keys.Right) + { + ViewModel.Next(); + e.Handled = true; + } + } + + protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) + { + base.OnPaintSurface(e); + + var canvas = e.Surface.Canvas; + canvas.Clear(SKColors.Gray); + + var info = e.Info; + + // Draw back image + if (ViewModel.BackImage != null) + { + renderer.DrawImage(canvas, ViewModel.BackImage, info, 1.0f); + } + + // Draw front image with fade animation + if (ViewModel.FrontImage != null && isAnimating) + { + renderer.DrawImage(canvas, ViewModel.FrontImage, info, fadeProgress); + } + + // Draw UI overlay + if (ViewModel.ControlsVisible) + { + renderer.DrawOverlay(canvas, info, ViewModel, hoveredItem); + } + } + + protected override void OnMouseClick(MouseEventArgs e) + { + base.OnMouseClick(e); + + if (!ViewModel.ControlsVisible) return; + + // Check if play button was clicked + if (renderer.PlayButtonRect.Contains(e.Location)) + { + ViewModel.TogglePlayPause(); + return; + } + + // Check if left arrow area was clicked + if (renderer.LeftArrowRect.Contains(e.Location)) + { + ViewModel.Previous(); + return; + } + + // Check if right arrow area was clicked + if (renderer.RightArrowRect.Contains(e.Location)) + { + ViewModel.Next(); + return; + } + + // Check if download message was clicked + if (renderer.DownloadMessageRect.Contains(e.Location)) + { + ViewModel.InvokeDownload(); + return; + } + + // Check if carousel indicator was clicked + var clickedIndex = Array.FindIndex(renderer.CarouselIndicatorRects ?? [], r => r.Contains(e.Location)); + if (clickedIndex != -1) + { + ViewModel.SelectedIndex = clickedIndex; + return; + } + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + + if (!ViewModel.ControlsVisible) + { + Cursor = Cursors.Default; + return; + } + + var previousHoveredItem = hoveredItem; + hoveredItem = HoveredItem.None; + + // Check UI elements in priority order + if (renderer.PlayButtonRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.PlayButton; + } + else if (renderer.DownloadMessageRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.DownloadButton; + } + else if (renderer.LeftArrowRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.LeftArrow; + } + else if (renderer.RightArrowRect.Contains(e.Location)) + { + hoveredItem = HoveredItem.RightArrow; + } + + // Check carousel indicators for hand cursor + bool isOverCarouselIndicator = renderer.CarouselIndicatorRects?.Any(r => r.Contains(e.Location)) ?? false; + bool isOverClickable = hoveredItem != HoveredItem.None || isOverCarouselIndicator; + Cursor = isOverClickable ? Cursors.Hand : Cursors.Default; + + if (hoveredItem != previousHoveredItem) + { + Invalidate(); + } + } + + private void OnMouseLeave(object sender, EventArgs e) + { + ViewModel.IsMouseOver = false; + + if (hoveredItem != HoveredItem.None) + { + hoveredItem = HoveredItem.None; + Cursor = Cursors.Default; + Invalidate(); + } + } + + private void StartAnimation() + { + fadeProgress = 0f; + isAnimating = true; + animationStartTime = DateTime.Now; + fadeTimer.Start(); + } + + private void StopAnimation() + { + fadeTimer.Stop(); + isAnimating = false; + fadeProgress = 0f; + Invalidate(); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + var elapsed = DateTime.Now - animationStartTime; + fadeProgress = Math.Min(1.0f, (float)(elapsed.TotalMilliseconds / ANIMATION_DURATION_MS)); + + // Ease in-out sine function + fadeProgress = (float)(Math.Sin((fadeProgress - 0.5) * Math.PI) / 2 + 0.5); + + Invalidate(); + + if (fadeProgress >= 1.0f) + { + fadeTimer.Stop(); + isAnimating = false; + ViewModel.OnAnimationComplete(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + fadeTimer?.Dispose(); + ViewModel?.Stop(); + renderer?.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/src/WPF/ThemePreviewerViewModel.cs b/src/Skia/ThemePreviewerViewModel.cs similarity index 90% rename from src/WPF/ThemePreviewerViewModel.cs rename to src/Skia/ThemePreviewerViewModel.cs index 7dba82f8..17eecf0c 100644 --- a/src/WPF/ThemePreviewerViewModel.cs +++ b/src/Skia/ThemePreviewerViewModel.cs @@ -1,470 +1,467 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Windows.Input; -using System.Windows.Media.Imaging; -using System.Windows.Threading; - -namespace WinDynamicDesktop.WPF -{ - public class ThemePreviewItem - { - public string PreviewText { get; set; } - public Uri Uri { get; set; } - - public ThemePreviewItem(string previewText, string path) - { - PreviewText = previewText; - - string fullPath = Path.GetFullPath(path); - if (File.Exists(fullPath)) - { - Uri = new Uri(fullPath, UriKind.Absolute); - } - else - { - Uri = new Uri(path, UriKind.Relative); - } - } - } - - public class ThemePreviewerViewModel : INotifyPropertyChanged - { - #region Properties - - public bool ControlsVisible => !string.IsNullOrEmpty(Title); - public bool MessageVisible => !string.IsNullOrEmpty(Message); - public bool CarouselIndicatorsVisible => string.IsNullOrEmpty(Message); - public bool DownloadSizeVisible => !string.IsNullOrEmpty(DownloadSize); - - private string title; - public string Title - { - get => title; - set - { - SetProperty(ref title, value); - OnPropertyChanged(nameof(ControlsVisible)); - } - } - - private string author; - public string Author - { - get => author; - set => SetProperty(ref author, value); - } - - private string previewText; - public string PreviewText - { - get => previewText; - set => SetProperty(ref previewText, value); - } - - private string message; - public string Message - { - get => message; - set - { - SetProperty(ref message, value); - OnPropertyChanged(nameof(MessageVisible)); - OnPropertyChanged(nameof(CarouselIndicatorsVisible)); - } - } - - private Action downloadAction; - public Action DownloadAction - { - get => downloadAction; - set => SetProperty(ref downloadAction, value); - } - - private string downloadSize; - public string DownloadSize - { - get => downloadSize; - set => SetProperty(ref downloadSize, value); - } - - private BitmapImage backImage; - public BitmapImage BackImage - { - get => backImage; - set => SetProperty(ref backImage, value); - } - - private BitmapImage frontImage; - public BitmapImage FrontImage - { - get => frontImage; - set => SetProperty(ref frontImage, value); - } - - private bool isPlaying; - public bool IsPlaying - { - get => isPlaying; - set => SetProperty(ref isPlaying, value); - } - - private bool isMouseOver; - public bool IsMouseOver - { - get => isMouseOver; - set - { - SetProperty(ref isMouseOver, value); - if (value) - { - transitionTimer.Stop(); - } - else if (IsPlaying && fadeQueue.IsEmpty) - { - transitionTimer.Start(); - } - } - } - - private int selectedIndex; - public int SelectedIndex - { - get => selectedIndex; - set - { - if (value != selectedIndex) - { - GoTo(value); - } - SetProperty(ref selectedIndex, value); - } - } - - public ObservableCollection Items { get; } = new ObservableCollection(); - - #endregion - - #region Commands - - public ICommand PlayCommand => new RelayCommand(() => - { - IsPlaying = !IsPlaying; - if (IsPlaying && fadeQueue.IsEmpty) - { - transitionTimer.Start(); - } - }); - - public ICommand PreviousCommand => new RelayCommand(Previous); - - public ICommand NextCommand => new RelayCommand(Next); - - public ICommand DownloadCommand => new RelayCommand(() => DownloadAction?.Invoke()); - - #endregion - - private static readonly Func _ = Localization.GetTranslation; - - private const int TRANSITION_TIME = 5; - - private readonly BitmapCache cache = new BitmapCache(); - private readonly DispatcherTimer transitionTimer; - private readonly ConcurrentQueue fadeQueue = new ConcurrentQueue(); - private readonly SemaphoreSlim fadeSemaphore = new SemaphoreSlim(1, 1); - private readonly Action startAnimation; - private readonly Action stopAnimation; - - public ThemePreviewerViewModel(Action startAnimation, Action stopAnimation) - { - this.startAnimation = startAnimation; - this.stopAnimation = stopAnimation; - - transitionTimer = new DispatcherTimer(DispatcherPriority.Send) - { - Interval = TimeSpan.FromSeconds(TRANSITION_TIME) - }; - transitionTimer.Tick += (s, e) => Next(); - - IsPlaying = true; - } - - public void OnAnimationComplete() - { - BackImage = FrontImage; - FrontImage = null; - - int nextIndex = -1; - while (fadeQueue.TryDequeue(out int index)) - { - nextIndex = index; - } - - if (nextIndex != -1) - { - FrontImage = cache[Items[nextIndex].Uri]; - startAnimation(); - } - else - { - TryRelease(fadeSemaphore); - - if (IsPlaying && !IsMouseOver) - { - transitionTimer.Start(); - } - } - } - - public void PreviewTheme(ThemeConfig theme, Action downloadAction, string imagePath = null) - { - Stop(); - - int activeImage = 0; - string[] sunrise = null; - string[] day = null; - string[] sunset = null; - string[] night = null; - - if (theme != null) - { - DownloadAction = () => downloadAction.Invoke(theme); - Title = ThemeManager.GetThemeName(theme); - Author = ThemeManager.GetThemeAuthor(theme); - bool isDownloaded = ThemeManager.IsThemeDownloaded(theme); - - if (isDownloaded) - { - ThemeManager.CalcThemeInstallSize(theme, size => { DownloadSize = size; }); - - List imageTimes = SolarScheduler.GetAllImageTimes(theme); - activeImage = imageTimes.FindLastIndex((time) => time <= DateTime.Now); - if (activeImage == -1) - { - activeImage = imageTimes.FindLastIndex((time) => time.AddDays(-1) <= DateTime.Now); - } - - if (theme.sunriseImageList != null && !theme.sunriseImageList.SequenceEqual(theme.dayImageList)) - { - sunrise = ImagePaths(theme, theme.sunriseImageList); - AddItems(_("Sunrise"), sunrise, imageTimes.Take(theme.sunriseImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.sunriseImageList.Length); - } - - day = ImagePaths(theme, theme.dayImageList); - AddItems(_("Day"), day, imageTimes.Take(theme.dayImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.dayImageList.Length); - - if (theme.sunsetImageList != null && !theme.sunsetImageList.SequenceEqual(theme.dayImageList)) - { - sunset = ImagePaths(theme, theme.sunsetImageList); - AddItems(_("Sunset"), sunset, imageTimes.Take(theme.sunsetImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.sunsetImageList.Length); - } - - night = ImagePaths(theme, theme.nightImageList); - AddItems(_("Night"), night, imageTimes.Take(theme.nightImageList.Length).ToArray()); - imageTimes.RemoveRange(0, theme.nightImageList.Length); - } - else - { - Message = _("Theme is not downloaded. Click here to download and enable full preview."); - ThemeManager.CalcThemeDownloadSize(theme, size => { DownloadSize = size; }); - - string[] resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames(); - string path = "WinDynamicDesktop.resources.images." + theme.themeId + "_{0}.jpg"; - - string rsrcName = string.Format(path, "sunrise"); - if (resourceNames.Contains(rsrcName)) - { - sunrise = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "day"); - if (resourceNames.Contains(rsrcName)) - { - day = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "sunset"); - if (resourceNames.Contains(rsrcName)) - { - sunset = new[] { rsrcName }; - } - - rsrcName = string.Format(path, "night"); - if (resourceNames.Contains(rsrcName)) - { - night = new[] { rsrcName }; - } - - AddItems(_("Sunrise"), sunrise, null); - AddItems(_("Day"), day, null); - AddItems(_("Sunset"), sunset, null); - AddItems(_("Night"), night, null); - - SolarData solarData = SunriseSunsetService.GetSolarData(DateTime.Today); - DaySegmentData segmentData = SolarScheduler.GetDaySegmentData(solarData, DateTime.Now); - activeImage = (sunrise != null && sunset != null) ? segmentData.segment4 : segmentData.segment2; - } - } - else - { - Author = "Microsoft"; - Items.Add(new ThemePreviewItem(string.Empty, imagePath)); - activeImage = 0; - } - - Start(activeImage); - } - - private void Previous() - { - if (SelectedIndex == 0) - { - SelectedIndex = Items.Count - 1; - } - else - { - SelectedIndex--; - } - } - - private void Next() - { - if (SelectedIndex == Items.Count - 1) - { - SelectedIndex = 0; - } - else - { - SelectedIndex++; - } - } - - private void GoTo(int index) - { - if (index < 0 || index >= Items.Count) return; - - transitionTimer.Stop(); - - if (fadeSemaphore.Wait(0)) - { - FrontImage = cache[Items[index].Uri]; - startAnimation(); - } - else - { - fadeQueue.Enqueue(index); - } - - PreviewText = Items[index].PreviewText; - } - - public void Stop() - { - stopAnimation(); - while (fadeQueue.TryDequeue(out int temp)) { } - TryRelease(fadeSemaphore); - - transitionTimer.Stop(); - - Title = null; - Author = null; - PreviewText = null; - Message = null; - DownloadSize = null; - BackImage = null; - FrontImage = null; - SelectedIndex = -1; - - Items.Clear(); - cache.Clear(); - } - - private void AddItems(string previewName, string[] items, DateTime[] imageTimes) - { - if (items == null) return; - - for (int i = 0; i < items.Length; i++) - { - string previewText = previewName; - - if (imageTimes == null) // Theme not downloaded - { - previewText = string.Format(_("Previewing {0}"), previewName); - } - else if (imageTimes[i] == DateTime.MinValue) // Image not active - { - previewText = string.Format(_("Previewing {0} ({1}/{2})"), previewName, i + 1, items.Length); - } - else - { - previewText = string.Format(_("Previewing {0} at {1}"), previewName, imageTimes[i].ToShortTimeString()); - } - - Items.Add(new ThemePreviewItem(previewText, items[i])); - } - } - - private void Start(int index) - { - var item = Items[index]; - - PreviewText = item.PreviewText; - BackImage = cache[item.Uri]; - - selectedIndex = index; - OnPropertyChanged(nameof(SelectedIndex)); - - if (IsPlaying && !IsMouseOver) - { - transitionTimer.Start(); - } - } - - private static string[] ImagePaths(ThemeConfig theme, int[] imageList) - { - string themePath = ThemeManager.GetThemeDirectory(theme); - return imageList.Select(id => - Path.Combine(themePath, theme.imageFilename.Replace("*", id.ToString()))).ToArray(); - } - - private static void TryRelease(SemaphoreSlim semaphore) - { - try - { - semaphore.Release(); - } - catch (SemaphoreFullException) { } - } - - #region INotifyPropertyChanged - - public event PropertyChangedEventHandler PropertyChanged; - - private void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") - { - field = value; - OnPropertyChanged(propertyName); - } - - private void OnPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - #endregion - } -} +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using SkiaSharp; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace WinDynamicDesktop.Skia +{ + public class ThemePreviewItem + { + public string PreviewText { get; set; } + public Uri Uri { get; set; } + + public ThemePreviewItem(string previewText, string path) + { + PreviewText = previewText; + + string fullPath = Path.GetFullPath(path); + if (File.Exists(fullPath)) + { + Uri = new Uri(fullPath, UriKind.Absolute); + } + else + { + Uri = new Uri(path, UriKind.Relative); + } + } + } + + public class ThemePreviewerViewModel : INotifyPropertyChanged + { + #region Properties + + public bool ControlsVisible => !string.IsNullOrEmpty(Title); + public bool MessageVisible => !string.IsNullOrEmpty(Message); + public bool CarouselIndicatorsVisible => string.IsNullOrEmpty(Message); + public bool DownloadSizeVisible => !string.IsNullOrEmpty(DownloadSize); + + private string title; + public string Title + { + get => title; + set + { + SetProperty(ref title, value); + OnPropertyChanged(nameof(ControlsVisible)); + } + } + + private string author; + public string Author + { + get => author; + set => SetProperty(ref author, value); + } + + private string previewText; + public string PreviewText + { + get => previewText; + set => SetProperty(ref previewText, value); + } + + private string message; + public string Message + { + get => message; + set + { + SetProperty(ref message, value); + OnPropertyChanged(nameof(MessageVisible)); + OnPropertyChanged(nameof(CarouselIndicatorsVisible)); + } + } + + private Action downloadAction; + public Action DownloadAction + { + get => downloadAction; + set => SetProperty(ref downloadAction, value); + } + + private string downloadSize; + public string DownloadSize + { + get => downloadSize; + set => SetProperty(ref downloadSize, value); + } + + private SKImage backImage; + public SKImage BackImage + { + get => backImage; + set => SetProperty(ref backImage, value); + } + + private SKImage frontImage; + public SKImage FrontImage + { + get => frontImage; + set => SetProperty(ref frontImage, value); + } + + private bool isPlaying; + public bool IsPlaying + { + get => isPlaying; + set => SetProperty(ref isPlaying, value); + } + + private bool isMouseOver; + public bool IsMouseOver + { + get => isMouseOver; + set + { + SetProperty(ref isMouseOver, value); + if (value) + { + transitionTimer.Stop(); + } + else if (IsPlaying && fadeQueue.IsEmpty) + { + transitionTimer.Start(); + } + } + } + + private int selectedIndex; + public int SelectedIndex + { + get => selectedIndex; + set + { + if (value != selectedIndex) + { + GoTo(value); + } + SetProperty(ref selectedIndex, value); + } + } + + public ObservableCollection Items { get; } = new ObservableCollection(); + + #endregion + + #region Public Methods + + public void TogglePlayPause() + { + IsPlaying = !IsPlaying; + if (IsPlaying && fadeQueue.IsEmpty) + { + transitionTimer.Start(); + } + } + + public void InvokeDownload() + { + DownloadAction?.Invoke(); + } + + #endregion + + private static readonly Func _ = Localization.GetTranslation; + + private const int TRANSITION_TIME = 5; + + private readonly ImageCache cache = new ImageCache(); + private readonly System.Windows.Forms.Timer transitionTimer; + private readonly ConcurrentQueue fadeQueue = new ConcurrentQueue(); + private readonly SemaphoreSlim fadeSemaphore = new SemaphoreSlim(1, 1); + private readonly Action startAnimation; + private readonly Action stopAnimation; + + public ThemePreviewerViewModel(Action startAnimation, Action stopAnimation) + { + this.startAnimation = startAnimation; + this.stopAnimation = stopAnimation; + + transitionTimer = new System.Windows.Forms.Timer() + { + Interval = TRANSITION_TIME * 1000 + }; + transitionTimer.Tick += (s, e) => Next(); + + IsPlaying = true; + } + + public void OnAnimationComplete() + { + BackImage = FrontImage; + FrontImage = null; + + int nextIndex = -1; + while (fadeQueue.TryDequeue(out int index)) + { + nextIndex = index; + } + + if (nextIndex != -1) + { + FrontImage = cache[Items[nextIndex].Uri]; + startAnimation(); + } + else + { + TryRelease(fadeSemaphore); + + if (IsPlaying && !IsMouseOver) + { + transitionTimer.Start(); + } + } + } + + public void PreviewTheme(ThemeConfig theme, Action downloadAction, string imagePath = null) + { + Stop(); + + int activeImage = 0; + string[] sunrise = null; + string[] day = null; + string[] sunset = null; + string[] night = null; + + if (theme != null) + { + DownloadAction = () => downloadAction.Invoke(theme); + Title = ThemeManager.GetThemeName(theme); + Author = ThemeManager.GetThemeAuthor(theme); + bool isDownloaded = ThemeManager.IsThemeDownloaded(theme); + + if (isDownloaded) + { + ThemeManager.CalcThemeInstallSize(theme, size => { DownloadSize = size; }); + + List imageTimes = SolarScheduler.GetAllImageTimes(theme); + activeImage = imageTimes.FindLastIndex((time) => time <= DateTime.Now); + if (activeImage == -1) + { + activeImage = imageTimes.FindLastIndex((time) => time.AddDays(-1) <= DateTime.Now); + } + + if (theme.sunriseImageList != null && !theme.sunriseImageList.SequenceEqual(theme.dayImageList)) + { + sunrise = ImagePaths(theme, theme.sunriseImageList); + AddItems(_("Sunrise"), sunrise, imageTimes.Take(theme.sunriseImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.sunriseImageList.Length); + } + + day = ImagePaths(theme, theme.dayImageList); + AddItems(_("Day"), day, imageTimes.Take(theme.dayImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.dayImageList.Length); + + if (theme.sunsetImageList != null && !theme.sunsetImageList.SequenceEqual(theme.dayImageList)) + { + sunset = ImagePaths(theme, theme.sunsetImageList); + AddItems(_("Sunset"), sunset, imageTimes.Take(theme.sunsetImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.sunsetImageList.Length); + } + + night = ImagePaths(theme, theme.nightImageList); + AddItems(_("Night"), night, imageTimes.Take(theme.nightImageList.Length).ToArray()); + imageTimes.RemoveRange(0, theme.nightImageList.Length); + } + else + { + Message = _("Theme is not downloaded. Click here to download and enable full preview."); + ThemeManager.CalcThemeDownloadSize(theme, size => { DownloadSize = size; }); + + string[] resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames(); + string path = "WinDynamicDesktop.resources.images." + theme.themeId + "_{0}.jpg"; + + string rsrcName = string.Format(path, "sunrise"); + if (resourceNames.Contains(rsrcName)) + { + sunrise = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "day"); + if (resourceNames.Contains(rsrcName)) + { + day = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "sunset"); + if (resourceNames.Contains(rsrcName)) + { + sunset = new[] { rsrcName }; + } + + rsrcName = string.Format(path, "night"); + if (resourceNames.Contains(rsrcName)) + { + night = new[] { rsrcName }; + } + + AddItems(_("Sunrise"), sunrise, null); + AddItems(_("Day"), day, null); + AddItems(_("Sunset"), sunset, null); + AddItems(_("Night"), night, null); + + SolarData solarData = SunriseSunsetService.GetSolarData(DateTime.Today); + DaySegmentData segmentData = SolarScheduler.GetDaySegmentData(solarData, DateTime.Now); + activeImage = (sunrise != null && sunset != null) ? segmentData.segment4 : segmentData.segment2; + } + } + else + { + Author = "Microsoft"; + Items.Add(new ThemePreviewItem(string.Empty, imagePath)); + activeImage = 0; + } + + Start(activeImage); + } + + public void Previous() + { + if (SelectedIndex == 0) + { + SelectedIndex = Items.Count - 1; + } + else + { + SelectedIndex--; + } + } + + public void Next() + { + if (SelectedIndex == Items.Count - 1) + { + SelectedIndex = 0; + } + else + { + SelectedIndex++; + } + } + + private void GoTo(int index) + { + if (index < 0 || index >= Items.Count) return; + + transitionTimer.Stop(); + + if (fadeSemaphore.Wait(0)) + { + FrontImage = cache[Items[index].Uri]; + startAnimation(); + } + else + { + fadeQueue.Enqueue(index); + } + + PreviewText = Items[index].PreviewText; + } + + public void Stop() + { + stopAnimation(); + while (fadeQueue.TryDequeue(out int temp)) { } + TryRelease(fadeSemaphore); + + transitionTimer.Stop(); + + Title = null; + Author = null; + PreviewText = null; + Message = null; + DownloadSize = null; + BackImage = null; + FrontImage = null; + SelectedIndex = -1; + + Items.Clear(); + cache.Clear(); + } + + private void AddItems(string previewName, string[] items, DateTime[] imageTimes) + { + if (items == null) return; + + for (int i = 0; i < items.Length; i++) + { + string previewText = previewName; + + if (imageTimes == null) // Theme not downloaded + { + previewText = string.Format(_("Previewing {0}"), previewName); + } + else if (imageTimes[i] == DateTime.MinValue) // Image not active + { + previewText = string.Format(_("Previewing {0} ({1}/{2})"), previewName, i + 1, items.Length); + } + else + { + previewText = string.Format(_("Previewing {0} at {1}"), previewName, imageTimes[i].ToShortTimeString()); + } + + Items.Add(new ThemePreviewItem(previewText, items[i])); + } + } + + private void Start(int index) + { + var item = Items[index]; + + PreviewText = item.PreviewText; + BackImage = cache[item.Uri]; + + selectedIndex = index; + OnPropertyChanged(nameof(SelectedIndex)); + + if (IsPlaying && !IsMouseOver) + { + transitionTimer.Start(); + } + } + + private static string[] ImagePaths(ThemeConfig theme, int[] imageList) + { + string themePath = ThemeManager.GetThemeDirectory(theme); + return imageList.Select(id => + Path.Combine(themePath, theme.imageFilename.Replace("*", id.ToString()))).ToArray(); + } + + private static void TryRelease(SemaphoreSlim semaphore) + { + try + { + semaphore.Release(); + } + catch (SemaphoreFullException) { } + } + + #region INotifyPropertyChanged + + public event PropertyChangedEventHandler PropertyChanged; + + private void SetProperty(ref T field, T value, [CallerMemberName] string propertyName = "") + { + field = value; + OnPropertyChanged(propertyName); + } + + private void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + } +} diff --git a/src/ThemeDialog.Designer.cs b/src/ThemeDialog.Designer.cs index 14d53c5b..4c513ebd 100644 --- a/src/ThemeDialog.Designer.cs +++ b/src/ThemeDialog.Designer.cs @@ -40,7 +40,7 @@ private void InitializeComponent() toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); showInstalledMenuItem = new System.Windows.Forms.ToolStripMenuItem(); displayComboBox = new System.Windows.Forms.ComboBox(); - previewerHost = new System.Windows.Forms.Integration.ElementHost(); + previewerHost = new WinDynamicDesktop.Skia.ThemePreviewer(); listView1 = new System.Windows.Forms.ListView(); advancedButton = new System.Windows.Forms.Button(); searchBox = new System.Windows.Forms.TextBox(); @@ -248,7 +248,7 @@ private void InitializeComponent() private System.Windows.Forms.OpenFileDialog openFileDialog1; private System.Windows.Forms.ContextMenuStrip contextMenuStrip1; private System.Windows.Forms.ToolStripMenuItem favoriteThemeMenuItem; - private System.Windows.Forms.Integration.ElementHost previewerHost; + private Skia.ThemePreviewer previewerHost; private System.Windows.Forms.ListView listView1; private System.Windows.Forms.ToolStripMenuItem deleteThemeMenuItem; private System.Windows.Forms.ComboBox displayComboBox; diff --git a/src/ThemeDialog.cs b/src/ThemeDialog.cs index d9ef8204..75e4af8e 100644 --- a/src/ThemeDialog.cs +++ b/src/ThemeDialog.cs @@ -24,8 +24,6 @@ public partial class ThemeDialog : Form private readonly string windowsWallpaper = ThemeThumbLoader.GetWindowsWallpaper(false); private readonly string windowsLockScreen = ThemeThumbLoader.GetWindowsWallpaper(true); - private WPF.ThemePreviewer previewer; - public ThemeDialog() { InitializeComponent(); @@ -188,7 +186,7 @@ private void UpdateSelectedItem() { if (listView1.Items.Count == 0) { - previewer.ViewModel.PreviewTheme(null, null, + previewerHost.ViewModel.PreviewTheme(null, null, ThemeThumbLoader.GetWindowsWallpaper(IsLockScreenSelected)); } applyButton.Enabled = false; @@ -208,15 +206,12 @@ private void UpdateSelectedItem() } applyButton.Enabled = true; - previewer.ViewModel.PreviewTheme(theme, new Action((theme) => DownloadTheme(theme)), + previewerHost.ViewModel.PreviewTheme(theme, new Action((theme) => DownloadTheme(theme)), ThemeThumbLoader.GetWindowsWallpaper(IsLockScreenSelected)); } private void ThemeDialog_Load(object sender, EventArgs e) { - previewer = new WPF.ThemePreviewer(); - previewerHost.Child = previewer; - listView1.ContextMenuStrip = contextMenuStrip1; listView1.ListViewItemSorter = new CompareByItemText(); ThemeDialogUtils.SetWindowTheme(listView1.Handle, "Explorer", null); @@ -530,7 +525,7 @@ private void OnFormClosing(object sender, FormClosingEventArgs e) private void OnFormClosed(object sender, FormClosedEventArgs e) { - this.Invoke(previewer.ViewModel.Stop); + this.Invoke(previewerHost.ViewModel.Stop); } } diff --git a/src/WPF/BitmapCache.cs b/src/WPF/BitmapCache.cs deleted file mode 100644 index 309e4c31..00000000 --- a/src/WPF/BitmapCache.cs +++ /dev/null @@ -1,99 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Windows.Forms; -using System.Windows.Media.Imaging; - -namespace WinDynamicDesktop.WPF -{ - sealed class BitmapCache - { - readonly int decodeWidth; - readonly int decodeHeight; - readonly object cacheLock = new object(); - readonly Dictionary images = new Dictionary(); - - public BitmapImage this[Uri uri] - { - get - { - lock (cacheLock) - { - if (images.ContainsKey(uri)) - { - return images[uri]; - } - else - { - var img = CreateImage(uri); - images.Add(uri, img); - return img; - } - } - } - } - - public void Clear() - { - foreach (var uri in images.Keys.ToList()) - { - images.Remove(uri); - } - GC.Collect(); - } - - public BitmapCache(bool limitDecodeSize = true) - { - if (limitDecodeSize) - { - int maxArea = 0; - foreach (Screen screen in Screen.AllScreens) - { - int area = screen.Bounds.Width * screen.Bounds.Height; - if (area > maxArea) - { - maxArea = area; - decodeWidth = screen.Bounds.Width; - decodeHeight = screen.Bounds.Height; - } - } - } - } - - private BitmapImage CreateImage(Uri uri) - { - BitmapImage img = new BitmapImage(); - img.BeginInit(); - img.CacheOption = BitmapCacheOption.OnLoad; - img.CreateOptions = BitmapCreateOptions.IgnoreColorProfile; - - if (uri.IsAbsoluteUri) - { - img.UriSource = uri; - } - else - { - img.StreamSource = Assembly.GetExecutingAssembly().GetManifestResourceStream(uri.OriginalString); - } - - if (decodeWidth >= decodeHeight) - { - img.DecodePixelWidth = decodeWidth; - } - else - { - img.DecodePixelHeight = decodeHeight; - } - - img.EndInit(); - img.StreamSource?.Dispose(); - img.Freeze(); - return img; - } - } -} diff --git a/src/WPF/RelayCommand.cs b/src/WPF/RelayCommand.cs deleted file mode 100644 index e4871d7e..00000000 --- a/src/WPF/RelayCommand.cs +++ /dev/null @@ -1,37 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Windows.Input; - -namespace WinDynamicDesktop.WPF -{ - public class RelayCommand : ICommand - { - private readonly Action execute; - private readonly Func canExecute; - - public event EventHandler CanExecuteChanged - { - add => CommandManager.RequerySuggested += value; - remove => CommandManager.RequerySuggested -= value; - } - - public RelayCommand(Action execute, Func canExecute = null) - { - this.execute = execute; - this.canExecute = canExecute; - } - - public bool CanExecute(object parameter) - { - return canExecute?.Invoke() ?? true; - } - - public void Execute(object parameter) - { - execute?.Invoke(); - } - } -} diff --git a/src/WPF/ThemePreviewer.xaml b/src/WPF/ThemePreviewer.xaml deleted file mode 100644 index 541a1769..00000000 --- a/src/WPF/ThemePreviewer.xaml +++ /dev/null @@ -1,157 +0,0 @@ - - - pack://application:,,,/resources/fonts/fontawesome-webfont.ttf#FontAwesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/WPF/ThemePreviewer.xaml.cs b/src/WPF/ThemePreviewer.xaml.cs deleted file mode 100644 index 851a5570..00000000 --- a/src/WPF/ThemePreviewer.xaml.cs +++ /dev/null @@ -1,54 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.ComponentModel; -using System.Windows; -using System.Windows.Media.Animation; -using System.Windows.Threading; - -namespace WinDynamicDesktop.WPF -{ - public partial class ThemePreviewer - { - public ThemePreviewerViewModel ViewModel { get; } - - private readonly Storyboard fadeAnimation; - private readonly DispatcherTimer triggerTimer; - - public ThemePreviewer() - { - ViewModel = new ThemePreviewerViewModel(StartAnimation, StopAnimation); - DataContext = ViewModel; - - InitializeComponent(); - - fadeAnimation = FindResource("FadeAnimation") as Storyboard; - fadeAnimation.Completed += (s, e) => ViewModel.OnAnimationComplete(); - - triggerTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(1.0 / 60) - }; - triggerTimer.Tick += (s, e) => - { - triggerTimer.Stop(); - fadeAnimation.Begin(FrontImage, true); - }; - - DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(IsMouseOverProperty, typeof(UIElement)); - descriptor.AddValueChanged(this, (s, e) => ViewModel.IsMouseOver = IsMouseOver); - } - - private void StartAnimation() - { - triggerTimer.Start(); - } - - private void StopAnimation() - { - fadeAnimation.Stop(FrontImage); - } - } -} diff --git a/src/WinDynamicDesktop.csproj b/src/WinDynamicDesktop.csproj index cd33cc95..3d2b44b9 100644 --- a/src/WinDynamicDesktop.csproj +++ b/src/WinDynamicDesktop.csproj @@ -16,11 +16,10 @@ true - true - + @@ -37,6 +36,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -57,4 +58,10 @@ - \ No newline at end of file + + + + + + +