diff --git a/.gitignore b/.gitignore index c5e41aa..22a8ddf 100644 --- a/.gitignore +++ b/.gitignore @@ -233,4 +233,14 @@ project.lock.json WebSrc/language-server-log.txt -._* \ No newline at end of file +._* + + +## Dev Licence Files + +**/devlics +**/devlics/* +*.devlic +*.license +language-server-log.txt +QuickDrawWindows/Properties/launchSettings.json diff --git a/QuickDraw.Core/Models/ImageFolder.cs b/QuickDraw.Core/Models/ImageFolder.cs new file mode 100644 index 0000000..9658a2d --- /dev/null +++ b/QuickDraw.Core/Models/ImageFolder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace QuickDraw.Core.Models; + +public class ImageFolder +{ + public string Path { get; } + public int ImageCount { get; set; } + + [JsonIgnore] + public bool IsLoading { get; set; } = false; + + public bool Selected { get; set; } = false; + + public ImageFolder(string path, int imageCount = 0, bool selected = false, bool isLoading = false) + { + Path = path; + ImageCount = imageCount; + Selected = selected; + IsLoading = isLoading; + } +} diff --git a/QuickDraw.Core/Models/ImageFolderList.cs b/QuickDraw.Core/Models/ImageFolderList.cs new file mode 100644 index 0000000..e77cd75 --- /dev/null +++ b/QuickDraw.Core/Models/ImageFolderList.cs @@ -0,0 +1,142 @@ +using System.Collections.Concurrent; +using System.Collections.Specialized; + +namespace QuickDraw.Core.Models; + +public class ImageFolderList : INotifyCollectionChanged +{ + public List ImageFolders { get; set; } = []; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + private static async Task> GetImagesForFolder(string filepath) + { + return await Task.Run(() => + { + var enumerationOptions = new EnumerationOptions + { + IgnoreInaccessible = true, + RecurseSubdirectories = true, + AttributesToSkip = System.IO.FileAttributes.Hidden | System.IO.FileAttributes.System | System.IO.FileAttributes.ReparsePoint + }; + + IEnumerable files = Directory.EnumerateFiles(filepath, "*.*", enumerationOptions) + .Where(s => s.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) + || s.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) + || s.EndsWith(".png", StringComparison.OrdinalIgnoreCase)); + + return files; + }); + } + + public static async Task> GetImagesForFolders(IEnumerable folders) + { + ConcurrentBag images = []; + + await Parallel.ForEachAsync(folders, async (folder, ct) => + { + IEnumerable files = await GetImagesForFolder(folder); + + foreach (var file in files) + { + images.Add(file); + } + }); + + return images; + } + + public void UpdateFolderCount(ImageFolder existingFolder) + { + var folder = new ImageFolder(existingFolder.Path, existingFolder.ImageCount, existingFolder.Selected, true); + var folderIndex = ImageFolders.IndexOf(existingFolder); + + if (folderIndex == -1) + { + return; // Folder isn't in list, TODO: should log + } + + ImageFolders[folderIndex] = folder; + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Replace, folder, existingFolder, folderIndex)); + + LoadFolderCount(folder); + } + + public void UpdateFolderCounts() + { + List folders = [.. ImageFolders]; + foreach(var folder in folders) + { + UpdateFolderCount(folder); + } + } + + private void LoadFolderCount(ImageFolder folder) + { + Task.Run(async () => + { + await Task.Delay(200); + return await GetImagesForFolder(folder.Path); + }).ContinueWith((t) => + { + if (t.IsFaulted) + { + // Log error + } + else + { + folder.ImageCount = t.Result.Count(); + folder.IsLoading = false; + + var existingFolder = ImageFolders.FirstOrDefault((f) => f.Path == folder.Path); + var folderIndex = existingFolder != null ? ImageFolders.IndexOf(existingFolder) : -1; + + if (folderIndex != -1) + { + ImageFolders[folderIndex] = folder; + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Replace, folder, existingFolder, folderIndex)); + } + } + }); + } + + public void AddFolderPath(string path) + { + + var (folderIndex, existingFolder) = ImageFolders.Index().FirstOrDefault((ft) => ft.Item.Path == path); + ImageFolder? folder = null; + + if (existingFolder != null) + { + folder = new ImageFolder(existingFolder.Path, existingFolder.ImageCount, existingFolder.Selected, true); + ImageFolders[folderIndex] = folder; + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Replace, folder, existingFolder, folderIndex)); + } + else + { + folder = new ImageFolder(path, 0, false, true); + ImageFolders.Add(folder); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, folder)); + } + + if (folder != null) + { + LoadFolderCount(folder); + } + } + + public void AddFolderPaths(IEnumerable paths) + { + // TODO: Add paths in order, then load image counts in parallel + foreach (var path in paths) + { + AddFolderPath(path); + } + } + + public void RemoveFolder(ImageFolder folder) + { + ImageFolders.Remove(folder); + CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Remove, folder)); + } +} diff --git a/QuickDraw.Core/Models/Settings.cs b/QuickDraw.Core/Models/Settings.cs new file mode 100644 index 0000000..22e92dc --- /dev/null +++ b/QuickDraw.Core/Models/Settings.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace QuickDraw.Core.Models; + +public enum TimerEnum +{ + [Display(Name = "30s")] + T30s, + [Display(Name = "1m")] + T1m, + [Display(Name = "2m")] + T2m, + [Display(Name = "5m")] + T5m, + [Display(Name = "No Limit")] + NoLimit +}; + +public static class TimerEnumExtension +{ + private static Dictionary TimerEnumToSeconds { get; } = new() + { + { TimerEnum.T30s, 30 }, + { TimerEnum.T1m, 60 }, + { TimerEnum.T2m, 120 }, + { TimerEnum.T5m, 300 }, + { TimerEnum.NoLimit, 0 } + }; + + public static uint ToSeconds(this TimerEnum e) + { + return TimerEnumToSeconds[e]; + } + + public static double ToSliderValue(this TimerEnum e) + { + return (double)((int)e); + } + + public static TimerEnum ToTimerEnum(this double e) + { + return (TimerEnum)(Math.Clamp((int)e,0, 4)); + } +} + +public class Settings +{ + public ImageFolderList ImageFolderList { get; set; } = new ImageFolderList(); + + public TimerEnum SlideTimerDuration { get; set; } +} diff --git a/QuickDraw.Core/QuickDraw.Core.csproj b/QuickDraw.Core/QuickDraw.Core.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/QuickDraw.Core/QuickDraw.Core.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/QuickDraw.sln b/QuickDraw.sln index da69f1c..8b5e9e9 100644 --- a/QuickDraw.sln +++ b/QuickDraw.sln @@ -1,25 +1,67 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31025.218 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11010.61 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickDraw", "QuickDrawWindows\QuickDraw.csproj", "{31B19DB2-2047-44DC-8C27-A34C113171CF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickDraw", "QuickDrawWindows\QuickDraw.csproj", "{CBC43D79-6F32-480A-BC3A-54383C19E4DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickDraw.Core", "QuickDraw.Core\QuickDraw.Core.csproj", "{DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {31B19DB2-2047-44DC-8C27-A34C113171CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {31B19DB2-2047-44DC-8C27-A34C113171CF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {31B19DB2-2047-44DC-8C27-A34C113171CF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {31B19DB2-2047-44DC-8C27-A34C113171CF}.Release|Any CPU.Build.0 = Release|Any CPU + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|Any CPU.ActiveCfg = Debug|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|Any CPU.Build.0 = Debug|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|ARM64.Build.0 = Debug|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x64.ActiveCfg = Debug|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x64.Build.0 = Debug|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x64.Deploy.0 = Debug|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x86.ActiveCfg = Debug|x86 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x86.Build.0 = Debug|x86 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Debug|x86.Deploy.0 = Debug|x86 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|Any CPU.ActiveCfg = Release|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|Any CPU.Build.0 = Release|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|ARM64.ActiveCfg = Release|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|ARM64.Build.0 = Release|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|ARM64.Deploy.0 = Release|ARM64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x64.ActiveCfg = Release|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x64.Build.0 = Release|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x64.Deploy.0 = Release|x64 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x86.ActiveCfg = Release|x86 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x86.Build.0 = Release|x86 + {CBC43D79-6F32-480A-BC3A-54383C19E4DD}.Release|x86.Deploy.0 = Release|x86 + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|ARM64.Build.0 = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|x64.Build.0 = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Debug|x86.Build.0 = Debug|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|Any CPU.Build.0 = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|ARM64.ActiveCfg = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|ARM64.Build.0 = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|x64.ActiveCfg = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|x64.Build.0 = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|x86.ActiveCfg = Release|Any CPU + {DC397900-3CE2-426A-B15B-A7CA3C2EEE2C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {03EDC6A8-A6C7-4B32-B248-CB1593722EC5} + SolutionGuid = {9BC0E5F9-D71B-443D-90F5-DBF6C0A50453} EndGlobalSection EndGlobal diff --git a/QuickDrawWindows/Activation/ActivationHandler.cs b/QuickDrawWindows/Activation/ActivationHandler.cs new file mode 100644 index 0000000..277ca83 --- /dev/null +++ b/QuickDrawWindows/Activation/ActivationHandler.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace QuickDraw.Activation; + +public abstract class ActivationHandler : IActivationHandler + where T : class +{ + // Override this method to add the logic for whether to handle the activation. + protected virtual bool CanHandleInternal(T args) => true; + + // Override this method to add the logic for your activation handler. + protected abstract Task HandleInternalAsync(T args); + + public bool CanHandle(object args) => args is T && CanHandleInternal((args as T)!); + + public async Task HandleAsync(object args) => await HandleInternalAsync((args as T)!); +} diff --git a/QuickDrawWindows/Activation/DefaultActivationHandler.cs b/QuickDrawWindows/Activation/DefaultActivationHandler.cs new file mode 100644 index 0000000..4358d0f --- /dev/null +++ b/QuickDrawWindows/Activation/DefaultActivationHandler.cs @@ -0,0 +1,26 @@ +using Microsoft.UI.Xaml; +using QuickDraw.Contracts.Services; +using QuickDraw.ViewModels; +using System.Threading.Tasks; + +namespace QuickDraw.Activation; + +public class DefaultActivationHandler : ActivationHandler +{ + private readonly INavigationService _navigationService; + + public DefaultActivationHandler(INavigationService navigationService) => _navigationService = navigationService; + + protected override bool CanHandleInternal(LaunchActivatedEventArgs args) + { + // None of the ActivationHandlers has handled the activation. + return _navigationService.Frame?.Content == null; + } + + protected async override Task HandleInternalAsync(LaunchActivatedEventArgs args) + { + _navigationService.NavigateTo(typeof(MainViewModel).FullName!, args.Arguments); + + await Task.CompletedTask; + } +} diff --git a/QuickDrawWindows/Activation/IActivationHandler.cs b/QuickDrawWindows/Activation/IActivationHandler.cs new file mode 100644 index 0000000..1ea99b9 --- /dev/null +++ b/QuickDrawWindows/Activation/IActivationHandler.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace QuickDraw.Activation; + +public interface IActivationHandler +{ + bool CanHandle(object args); + + Task HandleAsync(object args); +} diff --git a/QuickDrawWindows/App.xaml b/QuickDrawWindows/App.xaml index c2b1991..e0687e7 100644 --- a/QuickDrawWindows/App.xaml +++ b/QuickDrawWindows/App.xaml @@ -1,8 +1,19 @@ - + + - + + + + + + + + Transparent + Transparent + diff --git a/QuickDrawWindows/App.xaml.cs b/QuickDrawWindows/App.xaml.cs index 6c9ea91..bce2156 100644 --- a/QuickDrawWindows/App.xaml.cs +++ b/QuickDrawWindows/App.xaml.cs @@ -1,118 +1,98 @@ -using System; -using System.Diagnostics; -using System.Windows; -using Microsoft.Web.WebView2.Core; -using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml; +using QuickDraw.Activation; +using QuickDraw.Contracts.Services; +using QuickDraw.Services; +using QuickDraw.ViewModels; +using QuickDraw.Views; +using System; using System.IO; -using System.ComponentModel; -using System.Net.Http; +using System.Reflection; -namespace QuickDraw +namespace QuickDraw; + +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application + public IHost Host { - private readonly HttpClient httpClient = new(); - private InstallingWindow installingWindow; - - private string installerFile; + get; + } - private static bool HasWebView2() + public static T GetService() + where T : class + { + if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service) { - try - { - string versionString = CoreWebView2Environment.GetAvailableBrowserVersionString(); - Version requiredVersion = Version.Parse("89.0.774.75"); - Version version = Version.Parse(versionString.Split(" ")[0]); - - return version.CompareTo(requiredVersion) >= 0; - } - catch (WebView2RuntimeNotFoundException) - { - return false; - } + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); } - private async void DownloadWebView2() - { - using (var response = await httpClient.GetAsync("https://go.microsoft.com/fwlink/p/?LinkId=2124703", HttpCompletionOption.ResponseHeadersRead)) - { - if (response.IsSuccessStatusCode) - { - using (var stream = await response.Content.ReadAsStreamAsync()) - using (var fileStream = new FileStream(installerFile, FileMode.Create)) - { - await stream.CopyToAsync(fileStream); - } - DownloadRuntimeCompleted(); - } else - { - InstallError(); - } - - } - } + return service; + } - private void InstallError() + public App() + { + try { - installingWindow.Hide(); + string resourceName = "syncfusion.license"; - MessageBoxResult dialogResult = MessageBox.Show("Microsoft Edge WebView2 did not install properly. Click OK to try again or Cancel to quit.", - "QuickDraw", MessageBoxButton.OKCancel, MessageBoxImage.Exclamation); + Assembly assembly = Assembly.GetExecutingAssembly(); - if (dialogResult == MessageBoxResult.OK) + if (assembly != null) { - installingWindow.Show(); - if (File.Exists(installerFile)) + using Stream? rsrcStream = assembly.GetManifestResourceStream(assembly.GetName().Name + ".Assets." + resourceName); + + if (rsrcStream != null) { - File.Delete(installerFile); + using StreamReader streamReader = new(rsrcStream); + + string key = streamReader.ReadToEnd(); + + if (key != "") + { + Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense(key); + } } - DownloadWebView2(); - } - else - { - Shutdown(); } } + catch { }; - protected override /*async*/ void OnStartup(StartupEventArgs e) - { - string tempFolder = Path.GetTempPath(); - installerFile = Path.Combine(tempFolder, "MicrosoftEdgeWebview2Setup.exe"); + this.InitializeComponent(); - // Check for WebView2 Runtime, install if needed - if (HasWebView2()) + Host = Microsoft.Extensions.Hosting.Host. + CreateDefaultBuilder(). + UseContentRoot(AppContext.BaseDirectory). + ConfigureServices(services => { - MainWindow = new QuickDrawWindow(); - MainWindow.Show(); - } - else - { - installingWindow = new InstallingWindow(); - installingWindow.Show(); - DownloadWebView2(); - } - } + // Default Activation Handler + services.AddTransient, DefaultActivationHandler>(); - private async void DownloadRuntimeCompleted() - { - Process process = new(); - process.StartInfo.FileName = installerFile; - process.StartInfo.Arguments = @"/silent /install"; - process.StartInfo.Verb = "runas"; - process.StartInfo.UseShellExecute = true; - _ = process.Start(); - await process.WaitForExitAsync(); - - if (HasWebView2()) - { - installingWindow.Hide(); - MainWindow = new QuickDrawWindow(); - MainWindow.Show(); - return; - } - } + // Other Activation Handlers + + // Services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + }). + Build(); } + + protected async override void OnLaunched(LaunchActivatedEventArgs args) + { + base.OnLaunched(args); + + await App.GetService().ActivateAsync(args); + } + + public static MainWindow Window { get; } = new(); } diff --git a/QuickDrawWindows/AssemblyInfo.cs b/QuickDrawWindows/AssemblyInfo.cs deleted file mode 100644 index 8b5504e..0000000 --- a/QuickDrawWindows/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/QuickDrawWindows/Assets/LockScreenLogo.scale-200.png b/QuickDrawWindows/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/QuickDrawWindows/Assets/LockScreenLogo.scale-200.png differ diff --git a/QuickDrawWindows/QuickDraw.ico b/QuickDrawWindows/Assets/QuickDraw.ico similarity index 100% rename from QuickDrawWindows/QuickDraw.ico rename to QuickDrawWindows/Assets/QuickDraw.ico diff --git a/QuickDrawWindows/Assets/QuickDraw256x256.png b/QuickDrawWindows/Assets/QuickDraw256x256.png new file mode 100644 index 0000000..1dbe0fa Binary files /dev/null and b/QuickDrawWindows/Assets/QuickDraw256x256.png differ diff --git a/QuickDrawWindows/Assets/SplashScreen.scale-200.png b/QuickDrawWindows/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..32f486a Binary files /dev/null and b/QuickDrawWindows/Assets/SplashScreen.scale-200.png differ diff --git a/QuickDrawWindows/Assets/Square150x150Logo.scale-200.png b/QuickDrawWindows/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..53ee377 Binary files /dev/null and b/QuickDrawWindows/Assets/Square150x150Logo.scale-200.png differ diff --git a/QuickDrawWindows/Assets/Square44x44Logo.scale-200.png b/QuickDrawWindows/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..f713bba Binary files /dev/null and b/QuickDrawWindows/Assets/Square44x44Logo.scale-200.png differ diff --git a/QuickDrawWindows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/QuickDrawWindows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..dc9f5be Binary files /dev/null and b/QuickDrawWindows/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/QuickDrawWindows/Assets/StoreLogo.png b/QuickDrawWindows/Assets/StoreLogo.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/QuickDrawWindows/Assets/StoreLogo.png differ diff --git a/QuickDrawWindows/Assets/TitleBar.scale-100.png b/QuickDrawWindows/Assets/TitleBar.scale-100.png new file mode 100644 index 0000000..4ca48c0 Binary files /dev/null and b/QuickDrawWindows/Assets/TitleBar.scale-100.png differ diff --git a/QuickDrawWindows/Assets/TitleBar.scale-200.png b/QuickDrawWindows/Assets/TitleBar.scale-200.png new file mode 100644 index 0000000..321cd4b Binary files /dev/null and b/QuickDrawWindows/Assets/TitleBar.scale-200.png differ diff --git a/QuickDrawWindows/Assets/Wide310x150Logo.scale-200.png b/QuickDrawWindows/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..8b4a5d0 Binary files /dev/null and b/QuickDrawWindows/Assets/Wide310x150Logo.scale-200.png differ diff --git a/QuickDrawWindows/Contracts/Services/IActivationService.cs b/QuickDrawWindows/Contracts/Services/IActivationService.cs new file mode 100644 index 0000000..5c895cb --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/IActivationService.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace QuickDraw.Contracts.Services; + +public interface IActivationService +{ + Task ActivateAsync(object activationArgs); +} diff --git a/QuickDrawWindows/Contracts/Services/INavigationService.cs b/QuickDrawWindows/Contracts/Services/INavigationService.cs new file mode 100644 index 0000000..32a7bc4 --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/INavigationService.cs @@ -0,0 +1,26 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +using Microsoft.UI.Xaml.Navigation; + +namespace QuickDraw.Contracts.Services; + +public interface INavigationService +{ + event NavigatedEventHandler Navigated; + + bool CanGoBack + { + get; + } + + Frame? Frame + { + get; set; + } + + bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false, NavigationTransitionInfo? transitionInfo = null); + + bool GoBack(NavigationTransitionInfo? transitionInfo = null); + + void SetListDataItemForNextConnectedAnimation(object item); +} diff --git a/QuickDrawWindows/Contracts/Services/IPageService.cs b/QuickDrawWindows/Contracts/Services/IPageService.cs new file mode 100644 index 0000000..d4c2972 --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/IPageService.cs @@ -0,0 +1,8 @@ +using System; + +namespace QuickDraw.Contracts.Services; + +public interface IPageService +{ + Type GetPageType(string key); +} \ No newline at end of file diff --git a/QuickDrawWindows/Contracts/Services/ISettingsService.cs b/QuickDrawWindows/Contracts/Services/ISettingsService.cs new file mode 100644 index 0000000..f9b445b --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/ISettingsService.cs @@ -0,0 +1,13 @@ +using QuickDraw.Core.Models; +using System.Threading.Tasks; + +namespace QuickDraw.Contracts.Services; + +public interface ISettingsService +{ + public Settings? Settings { get; } + + public Task InitializeAsync(); + public Task ReadSettings(); + public Task WriteSettings(); +} diff --git a/QuickDrawWindows/Contracts/Services/ISlideImageService.cs b/QuickDrawWindows/Contracts/Services/ISlideImageService.cs new file mode 100644 index 0000000..cdc0935 --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/ISlideImageService.cs @@ -0,0 +1,15 @@ +using QuickDraw.Core.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace QuickDraw.Contracts.Services; + +public interface ISlideImageService +{ + public Task LoadImages(IEnumerable folders); + + public List Images { get; } + + public TimerEnum SlideDuration { get; set; } +} + diff --git a/QuickDrawWindows/Contracts/Services/ITitlebarService.cs b/QuickDrawWindows/Contracts/Services/ITitlebarService.cs new file mode 100644 index 0000000..d92b375 --- /dev/null +++ b/QuickDrawWindows/Contracts/Services/ITitlebarService.cs @@ -0,0 +1,15 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; + +namespace QuickDraw.Contracts.Services; + +public interface ITitlebarService +{ + public AppWindowTitleBar? TitleBar { get; } + + public GridLength LeftInset { get; } + public GridLength RightInset { get; } + + public void Initialize(Window window); + +} diff --git a/QuickDrawWindows/Contracts/ViewModels/INavigationAware.cs b/QuickDrawWindows/Contracts/ViewModels/INavigationAware.cs new file mode 100644 index 0000000..2839e94 --- /dev/null +++ b/QuickDrawWindows/Contracts/ViewModels/INavigationAware.cs @@ -0,0 +1,9 @@ +namespace QuickDraw.Contracts.ViewModels; + +public interface INavigationAware +{ + void OnNavigatedTo(object parameter); + + void OnNavigatedFrom(); +} + diff --git a/QuickDrawWindows/Contracts/ViewModels/IUnloadable.cs b/QuickDrawWindows/Contracts/ViewModels/IUnloadable.cs new file mode 100644 index 0000000..ba94100 --- /dev/null +++ b/QuickDrawWindows/Contracts/ViewModels/IUnloadable.cs @@ -0,0 +1,7 @@ +namespace QuickDraw.Contracts.ViewModels; + +public interface IUnloadable +{ + public void Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e); +} + diff --git a/QuickDrawWindows/Contracts/ViewModels/IViewModel.cs b/QuickDrawWindows/Contracts/ViewModels/IViewModel.cs new file mode 100644 index 0000000..943ec4c --- /dev/null +++ b/QuickDrawWindows/Contracts/ViewModels/IViewModel.cs @@ -0,0 +1,6 @@ +namespace QuickDraw.Contracts.ViewModels; + +public interface IViewModel +{ +} + diff --git a/QuickDrawWindows/DialogCenteringService.cs b/QuickDrawWindows/DialogCenteringService.cs deleted file mode 100644 index 7747145..0000000 --- a/QuickDrawWindows/DialogCenteringService.cs +++ /dev/null @@ -1,227 +0,0 @@ -using System; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace QuickDraw -{ - public sealed class DialogCenteringService : IDisposable - { - private readonly IWin32Window _owner; - private readonly Win32Native.HookProc _hookProc; - private IntPtr _hHook; - - public DialogCenteringService(IWin32Window owner) - { - _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - _hookProc = DialogHookProc; - _hHook = Win32Native.SetWindowsHookEx(Win32Native.WH_CALLWNDPROCRET, _hookProc, IntPtr.Zero, Win32Native.GetCurrentThreadId()); - } - - #region Disposing - ~DialogCenteringService() - { - Dispose(false); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - // if you have managed resources, get rid of them now - } - if (_hHook != IntPtr.Zero) - { - Win32Native.UnhookWindowsHookEx(_hHook); - _hHook = IntPtr.Zero; - } - } - #endregion - - private IntPtr DialogHookProc(int nCode, IntPtr wParam, IntPtr lParam) - { - if (nCode < 0) - { - return Win32Native.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); - } - - var msg = (Win32Native.CWPRETSTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32Native.CWPRETSTRUCT)); - - if (msg.message == (int)Win32Native.CbtHookAction.HCBT_ACTIVATE) - { - try - { - CenterWindow(msg.hwnd); - } - finally - { - Win32Native.UnhookWindowsHookEx(_hHook); - _hHook = IntPtr.Zero; - } - } - - return Win32Native.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); - } - - private bool CenterWindow(IntPtr hChildWnd) - { - var recParent = GetWindowRect(_owner.Handle); - return recParent != null ? CenterWindow(hChildWnd, recParent.Value) : false; - } - - private static Rectangle? GetWindowRect(IntPtr hWnd) - { - var rect = new Win32Native.RECT(); - if (Win32Native.GetWindowRect(hWnd, ref rect)) - { - return new Rectangle(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); - } - return null; - } - - private static bool CenterWindow(IntPtr hChildWnd, Rectangle recParent) - { - var recChild = GetWindowRect(hChildWnd); - if (recChild != null) - { - var centeredPoint = GetCenteredPoint(recParent, recChild.Value); - return Win32Native.SetWindowPos( - hChildWnd, - IntPtr.Zero, - centeredPoint.X, centeredPoint.Y, -1, -1, - Win32Native.SetWindowPosFlags.SWP_ASYNCWINDOWPOS | Win32Native.SetWindowPosFlags.SWP_NOSIZE | - Win32Native.SetWindowPosFlags.SWP_NOACTIVATE | Win32Native.SetWindowPosFlags.SWP_NOOWNERZORDER | - Win32Native.SetWindowPosFlags.SWP_NOZORDER); - } - return false; - } - - private static Point GetCenteredPoint(Rectangle recParent, Rectangle recChild) - { - var ptParentCenter = new Point - { - X = recParent.X + (recParent.Width / 2), - Y = recParent.Y + (recParent.Height / 2) - }; - - var ptStart = new Point - { - X = ptParentCenter.X - (recChild.Width / 2), - Y = ptParentCenter.Y - (recChild.Height / 2) - }; - - // get centered rectangle - var recCentered = new Rectangle(ptStart.X, ptStart.Y, recChild.Width, recChild.Height); - - // find the working area of the parent - var workingArea = Screen.FromRectangle(recParent).WorkingArea; - - // make sure child window isn't spanning across mulitiple screens - if (workingArea.X > recCentered.X) - { - recCentered = new Rectangle(workingArea.X, recCentered.Y, recCentered.Width, recCentered.Height); - } - if (workingArea.Y > recCentered.Y) - { - recCentered = new Rectangle(recCentered.X, workingArea.Y, recCentered.Width, recCentered.Height); - } - if (workingArea.Right < recCentered.Right) - { - recCentered = new Rectangle(workingArea.Right - recCentered.Width, recCentered.Y, recCentered.Width, recCentered.Height); - } - if (workingArea.Bottom < recCentered.Bottom) - { - recCentered = new Rectangle(recCentered.X, workingArea.Bottom - recCentered.Height, recCentered.Width, recCentered.Height); - } - - return new Point(recCentered.X, recCentered.Y); - } - - #region Native/Unsafe - private static class Win32Native - { - public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); - public const int WH_CALLWNDPROCRET = 12; - - public enum CbtHookAction - { - HCBT_MOVESIZE, - HCBT_MINMAX, - HCBT_QS, - HCBT_CREATEWND, - HCBT_DESTROYWND, - HCBT_ACTIVATE, - HCBT_CLICKSKIPPED, - HCBT_KEYSKIPPED, - HCBT_SYSCOMMAND, - HCBT_SETFOCUS - } - - [Flags] - public enum SetWindowPosFlags : uint - { - SWP_ASYNCWINDOWPOS = 0x4000U, - SWP_DEFERERASE = 0x2000U, - SWP_DRAWFRAME = 0x0020U, - SWP_FRAMECHANGED = 0x0020U, - SWP_HIDEWINDOW = 0x0080U, - SWP_NOACTIVATE = 0x0010U, - SWP_NOCOPYBITS = 0x0100U, - SWP_NOMOVE = 0x0002U, - SWP_NOOWNERZORDER = 0x0200U, - SWP_NOREDRAW = 0x0008U, - SWP_NOREPOSITION = 0x0200U, - SWP_NOSENDCHANGING = 0x0400U, - SWP_NOSIZE = 0x0001U, - SWP_NOZORDER = 0x0004U, - SWP_SHOWWINDOW = 0x0040U - } - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int left; - public int top; - public int right; - public int bottom; - } - - [StructLayout(LayoutKind.Sequential)] - public struct CWPRETSTRUCT - { - public IntPtr lResult; - public IntPtr lParam; - public IntPtr wParam; - public uint message; - public IntPtr hwnd; - }; - - [DllImport("kernel32.dll")] - public static extern int GetCurrentThreadId(); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SetWindowPosFlags uFlags); - - [DllImport("user32.dll")] - public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId); - - [DllImport("user32.dll")] - public static extern int UnhookWindowsHookEx(IntPtr idHook); - - [DllImport("user32.dll")] - public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam); - } - #endregion - } -} \ No newline at end of file diff --git a/QuickDrawWindows/FolderDialog.cs b/QuickDrawWindows/FolderDialog.cs deleted file mode 100644 index 146a0d2..0000000 --- a/QuickDrawWindows/FolderDialog.cs +++ /dev/null @@ -1,313 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace QuickDraw -{ - // Disable warning CS0108: 'x' hides inherited member 'y'. Use the new keyword if hiding was intended. - #pragma warning disable 0108 - - internal static class IIDGuid - { - internal const string IModalWindow = "b4db1657-70d7-485e-8e3e-6fcb5a5c1802"; - internal const string IFileDialog = "42f85136-db7e-439c-85f1-e4075d135fc8"; - internal const string IFileOpenDialog = "d57c7288-d4ad-4768-be02-9d969532d960"; - internal const string IShellItem = "43826d1e-e718-42ee-bc55-a1e261c37bfe"; - internal const string IShellItemArray = "B63EA76D-1F85-456F-A19C-48159EFA858B"; - } - - internal static class CLSIDGuid - { - internal const string FileOpenDialog = "DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7"; - internal const string ShellItem = "9ac9fbe1-e0a2-4ad6-b4ee-e212013ea917"; - } - - [Flags] - internal enum FileOpenDialogOptions : uint - { - NoChangeDir = 0x00000008, - PickFolders = 0x00000020, - AllowMultiSelect = 0x00000200, - PathMustExist = 0x00000800, - } - - public enum SIGDN : uint - { - NORMALDISPLAY = 0, - PARENTRELATIVEPARSING = 0x80018001, - PARENTRELATIVEFORADDRESSBAR = 0x8001c001, - DESKTOPABSOLUTEPARSING = 0x80028000, - PARENTRELATIVEEDITING = 0x80031001, - DESKTOPABSOLUTEEDITING = 0x8004c000, - FILESYSPATH = 0x80058000, - URL = 0x80068000 - } - - [ComImport] - [Guid(IIDGuid.IShellItem)] - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IShellItem - { - void BindToHandler(IntPtr pbc, - [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, - [MarshalAs(UnmanagedType.LPStruct)] Guid riid, - out IntPtr ppv); - - void GetParent(out IShellItem ppsi); - - void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName); - - void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); - - void Compare(IShellItem psi, uint hint, out int piOrder); - }; - - [ComImport, - Guid(IIDGuid.IShellItemArray), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - interface IShellItemArray - { - // Not supported: IBindCtx - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, - [In] ref Guid riid, out IntPtr ppvOut); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetPropertyDescriptionList([In] ref PropertyKey keyType, [In] ref Guid riid, out IntPtr ppv); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetAttributes([In] ShellItemArrayAttributeFlags dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetCount(out uint pdwNumItems); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - // Not supported: IEnumShellItems (will use GetCount and GetItemAt instead) - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); - } - - - - // These two are here for methods we don't use (but need to keep the function order), so they are empty - struct COMDLG_FILTERSPEC { } - - internal interface IFileDialogEvents { } - - internal enum FileDialogAddPosition : uint - { - - } - - public struct PropertyKey - { - - } - - public enum ShellItemArrayAttributeFlags - { - - } - - // End Empty Definitions - - - [ComImport(), - Guid(IIDGuid.IModalWindow), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IModalWindow - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), - PreserveSig] - int Show([In] IntPtr parent); - } - - [ComImport(), - Guid(IIDGuid.IFileDialog), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileDialog : IModalWindow - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), - PreserveSig] - int Show([In] IntPtr parent); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileTypes([In] uint cFileTypes, [In] COMDLG_FILTERSPEC[] rgFilterSpec); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileTypeIndex([In] uint iFileType); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFileTypeIndex(out uint piFileType); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Advise([In, MarshalAs(UnmanagedType.Interface)] IFileDialogEvents pfde, out uint pdwCookie); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Unadvise([In] uint dwCookie); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetOptions([In] FileOpenDialogOptions fos); - - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetOptions(out FileOpenDialogOptions pfos); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogAddPosition fdap); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Close([MarshalAs(UnmanagedType.Error)] int hr); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetClientGuid([In] ref Guid guid); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void ClearClientData(); - - // Not supported: IShellItemFilter is not defined, converting to IntPtr - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - } - - [ComImport(), - Guid(IIDGuid.IFileOpenDialog), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IFileOpenDialog : IFileDialog - { - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), - PreserveSig] - int Show([In] IntPtr parent); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileTypes([In] uint cFileTypes, [In] COMDLG_FILTERSPEC[] rgFilterSpec); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileTypeIndex([In] uint iFileType); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFileTypeIndex(out uint piFileType); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Advise([In, MarshalAs(UnmanagedType.Interface)] IFileDialogEvents pfde, out uint pdwCookie); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Unadvise([In] uint dwCookie); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetOptions([In] FileOpenDialogOptions fos); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetOptions(out FileOpenDialogOptions pfos); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogAddPosition fdap); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void Close([MarshalAs(UnmanagedType.Error)] int hr); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetClientGuid([In] ref Guid guid); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void ClearClientData(); - - // Not supported: IShellItemFilter is not defined, converting to IntPtr - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); - - // Defined by IFileOpenDialog - // --------------------------------------------------------------------------------- - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); - - [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] - void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); - } - - [ComImport, - ClassInterface(ClassInterfaceType.None), - TypeLibType(TypeLibTypeFlags.FCanCreate), - Guid(CLSIDGuid.FileOpenDialog)] - internal class FileOpenDialogRCW - { - } - - [ComImport, - Guid(IIDGuid.IFileOpenDialog), - CoClass(typeof(FileOpenDialogRCW))] - internal interface NativeFileOpenDialog : IFileOpenDialog - { - } -} \ No newline at end of file diff --git a/QuickDrawWindows/Installing.xaml b/QuickDrawWindows/Installing.xaml deleted file mode 100644 index 3e50ea3..0000000 --- a/QuickDrawWindows/Installing.xaml +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/QuickDrawWindows/Installing.xaml.cs b/QuickDrawWindows/Installing.xaml.cs deleted file mode 100644 index 49e0738..0000000 --- a/QuickDrawWindows/Installing.xaml.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; - -namespace QuickDraw -{ - /// - /// Interaction logic for Window1.xaml - /// - public partial class InstallingWindow : Window - { - public void OnClosed(object sender, EventArgs args) - { - ((App)Application.Current).Shutdown(); - } - - public InstallingWindow() - { - InitializeComponent(); - - this.Closed += OnClosed; - } - } -} diff --git a/QuickDrawWindows/MainWindow.xaml b/QuickDrawWindows/MainWindow.xaml index 6d7012c..d40fdc2 100644 --- a/QuickDrawWindows/MainWindow.xaml +++ b/QuickDrawWindows/MainWindow.xaml @@ -1,17 +1,16 @@ - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/QuickDrawWindows/MainWindow.xaml.cs b/QuickDrawWindows/MainWindow.xaml.cs index 25184ef..8a8e7ee 100644 --- a/QuickDrawWindows/MainWindow.xaml.cs +++ b/QuickDrawWindows/MainWindow.xaml.cs @@ -1,360 +1,27 @@ -using Microsoft.Web.WebView2.Core; -using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Animation; +using QuickDraw.Contracts.Services; +using QuickDraw.Views; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using System.Windows; -using System.IO; -using System.Runtime.InteropServices; -using System.Windows.Input; -using System.Net; +using Windows.UI; -namespace QuickDraw -{ - public class Message - { - public string type { get; set; } - } +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. - public class PathOpMessage : Message - { - public string path { get; set; } - } +namespace QuickDraw; - public class PathListOpMessage : Message - { - public List paths { get; set; } - } - - public class GetImagesMessage : PathListOpMessage - { - public int interval { get; set; } - } - - public struct ImageFolder - { - public string Path { get; set; } - - public int Count { get; set; } - } - - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class QuickDrawWindow : Window, System.Windows.Forms.IWin32Window +/// +/// An empty window that can be used on its own or navigated to within a Frame. +/// +public sealed partial class MainWindow : Window +{ + public MainWindow() { - private string domain; - public static readonly RoutedCommand StopPropagation = new(); - private void ExecutedStopPropagation(object sender, ExecutedRoutedEventArgs e) - { - // Do nothing, disabling this key combo - e.Handled = true; - } - - private void CanExecuteStopPropagation(object sender, CanExecuteRoutedEventArgs e) - { - e.CanExecute = true; - } - - private readonly Dictionary folderMappings = new(); - public IntPtr Handle => new System.Windows.Interop.WindowInteropHelper(this).Handle; - - private void WebViewUpdateFolders(List folders) - { - string jsonString = JsonSerializer.Serialize(new Dictionary - { - { "type", "UpdateFolders" }, - { "data", folders } - }); - - webView.CoreWebView2.PostWebMessageAsJson(jsonString); - } - - public void OnClosed(object sender, EventArgs args) - { - ((App)Application.Current).Shutdown(); - } - - public QuickDrawWindow() - { - InitializeComponent(); - - Closed += OnClosed; - - InitializeAsync(); - - } - - private async void InitializeAsync() - { - string userDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"QuickDraw"); - Directory.CreateDirectory(userDataFolder); - CoreWebView2EnvironmentOptions options = new(); - CoreWebView2Environment env = CoreWebView2Environment.CreateAsync("", userDataFolder, options).GetAwaiter().GetResult(); - - await webView.EnsureCoreWebView2Async(env); -#if DEBUG && !FAKE_RELEASE - domain = "http://localhost:8080"; -#else - webView.CoreWebView2.SetVirtualHostNameToFolderMapping( - "quickdraw.invalid", "WebSrc", - Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow - ); - -#if !FAKE_RELEASE - webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; - webView.CoreWebView2.Settings.IsZoomControlEnabled = false; - webView.CoreWebView2.Settings.AreDevToolsEnabled = false; - - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.G, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.G, ModifierKeys.Control | ModifierKeys.Shift))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.P, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.R, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F5))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.BrowserRefresh))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.R, ModifierKeys.Control | ModifierKeys.Shift))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F5, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F5, ModifierKeys.Shift))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.BrowserRefresh, ModifierKeys.Control))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.BrowserRefresh, ModifierKeys.Shift))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F3))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.F3, ModifierKeys.Shift))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.BrowserBack))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.Left, ModifierKeys.Alt))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.BrowserForward))); - webView.InputBindings.Add(new InputBinding(QuickDrawWindow.StopPropagation, new KeyGesture(Key.Right, ModifierKeys.Alt))); -#endif - - domain = "https://quickdraw.invalid"; -#endif - _ = webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync($@" - window.QuickDrawWindows = true; - "); - - webView.Source = new Uri($"{domain}/index.html"); - - webView.CoreWebView2.WebMessageReceived += ReceiveMessage; - } - - private static void OpenFolderInExplorer(string path) - { - if (Directory.Exists(path)) - { - ProcessStartInfo startInfo = new() - { - Arguments = path, - FileName = "explorer.exe" - }; - - _ = Process.Start(startInfo); - } - } - - private void OpenImageInExplorer(string path) - { - Uri imageUri = new(path); - string folder = folderMappings[imageUri.Host]; - string imagePath = Path.GetFullPath($"{folder}{WebUtility.UrlDecode(imageUri.AbsolutePath).Replace("/", "\\")}"); - Debug.WriteLine(imagePath); - - if (File.Exists(imagePath)) - { - ProcessStartInfo startInfo = new() - { - Arguments = $"/select,\"{imagePath}\"", - FileName = "explorer.exe" - }; - - _ = Process.Start(startInfo); - } - } - - private static IEnumerable GetFolderImages(string filepath) - { - IEnumerable files = Directory.EnumerateFiles(filepath, "*.*", SearchOption.AllDirectories) - .Where(s => s.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) - || s.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) - || s.EndsWith(".png", StringComparison.OrdinalIgnoreCase)); - - return files; - } - - private void GetImages(List folders, int interval) - { - HashSet images = new(); - // clear mappings - foreach (KeyValuePair mapping in folderMappings) - { - webView.CoreWebView2.ClearVirtualHostNameToFolderMapping(mapping.Key); - } - folderMappings.Clear(); - - int folderNum = 0; - - foreach (string folder in folders) - { - string hostName = $"quickdraw-folder{folderNum}.invalid"; - - webView.CoreWebView2.SetVirtualHostNameToFolderMapping( - hostName, folder, - CoreWebView2HostResourceAccessKind.Allow - ); - - folderMappings.Add(hostName, folder); - - IEnumerable files = GetFolderImages(folder).Select(p => p.Replace(folder, $"https://{hostName}").Replace("\\", "/").Replace("#", "%23")); - - images.UnionWith(files.ToHashSet()); - - folderNum++; - } - - if (images.Count > 0) - { - string jsonString = JsonSerializer.Serialize(new Dictionary - { - { "interval", interval * 1000 }, - { "images", images } - }); - - _ = webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync($@" - var slideshowData = {jsonString}; - "); - - webView.CoreWebView2.Navigate($"{domain}/slideshow.html"); - } - else - { - using DialogCenteringService centeringService = new(this); - _ = MessageBox.Show(this, "No images found! Select one or more folders.", "QuickDraw", MessageBoxButton.OK, MessageBoxImage.Exclamation); - } - } - - private void RefreshFolderCount(string path) - { - int count = GetFolderImages(path).Count(); - - if (count > 0) - { - WebViewUpdateFolders(new List { new ImageFolder { Path = path, Count = count } }); - } - } - - private void RefreshAllFolderCounts(List paths) - { - List folders = new(); - - foreach (string path in paths) - { - int count = GetFolderImages(path).Count(); - - if (count > 0) - { - folders.Add(new ImageFolder { Path = path, Count = count }); - } - } - - WebViewUpdateFolders(folders); - } - - private async void OpenFolders() - { - List folders = await Task.Run(() => - { - List folders = new(); - - IFileOpenDialog dialog = null; - uint count = 0; - try - { - dialog = new NativeFileOpenDialog(); - dialog.SetOptions( - FileOpenDialogOptions.NoChangeDir - | FileOpenDialogOptions.PickFolders - | FileOpenDialogOptions.AllowMultiSelect - | FileOpenDialogOptions.PathMustExist - ); - _ = dialog.Show(IntPtr.Zero); - - dialog.GetResults(out IShellItemArray shellItemArray); - - if (shellItemArray != null) - { - string filepath = null; - shellItemArray.GetCount(out count); - - for (uint i = 0; i < count; i++) - { - shellItemArray.GetItemAt(i, out IShellItem shellItem); - - if (shellItem != null) - { - shellItem.GetDisplayName(SIGDN.FILESYSPATH, out IntPtr i_result); - filepath = Marshal.PtrToStringAuto(i_result); - Marshal.FreeCoTaskMem(i_result); - - IEnumerable files = GetFolderImages(filepath); - - folders.Add(new ImageFolder { Path = filepath, Count = files.Count() }); - } - } - } - } - catch (COMException) - { - // No files or other weird error, do nothing. - } - finally - { - if (dialog != null) - { - _ = Marshal.FinalReleaseComObject(dialog); - } - } - return folders; - }); - - if (folders.Count > 0) - { - WebViewUpdateFolders(folders); - } - } - - private void ReceiveMessage(object sender, CoreWebView2WebMessageReceivedEventArgs args) - { - Message message = JsonSerializer.Deserialize(args.WebMessageAsJson); + this.InitializeComponent(); - switch (message.type) - { - case "addFolders": - OpenFolders(); - break; - case "refreshFolder": - PathOpMessage refreshFolderMessage = JsonSerializer.Deserialize(args.WebMessageAsJson); - RefreshFolderCount(refreshFolderMessage.path); - break; - case "refreshFolders": - PathListOpMessage refreshFoldersMessage = JsonSerializer.Deserialize(args.WebMessageAsJson); - RefreshAllFolderCounts(refreshFoldersMessage.paths); - break; - case "openFolder": - PathOpMessage openFolderMessage = JsonSerializer.Deserialize(args.WebMessageAsJson); - OpenFolderInExplorer(openFolderMessage.path); - break; - case "getImages": - GetImagesMessage getImagesMessage = JsonSerializer.Deserialize(args.WebMessageAsJson); - GetImages(getImagesMessage.paths, getImagesMessage.interval); - break; - case "openImage": - PathOpMessage openImageMessage = JsonSerializer.Deserialize(args.WebMessageAsJson); - OpenImageInExplorer(openImageMessage.path); - break; - default: - break; - } - } + // Do this here so it's appearance is correct from the hop + App.GetService().Initialize(this); } } diff --git a/QuickDrawWindows/Properties/PublishProfiles/win10-arm64.pubxml b/QuickDrawWindows/Properties/PublishProfiles/win10-arm64.pubxml new file mode 100644 index 0000000..ec5dadf --- /dev/null +++ b/QuickDrawWindows/Properties/PublishProfiles/win10-arm64.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + ARM64 + win-arm64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + False + False + False + True + + + \ No newline at end of file diff --git a/QuickDrawWindows/Properties/PublishProfiles/win10-x64.pubxml b/QuickDrawWindows/Properties/PublishProfiles/win10-x64.pubxml new file mode 100644 index 0000000..ee4d320 --- /dev/null +++ b/QuickDrawWindows/Properties/PublishProfiles/win10-x64.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + x64 + win-x64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + False + False + False + True + + + \ No newline at end of file diff --git a/QuickDrawWindows/Properties/PublishProfiles/win10-x86.pubxml b/QuickDrawWindows/Properties/PublishProfiles/win10-x86.pubxml new file mode 100644 index 0000000..756a190 --- /dev/null +++ b/QuickDrawWindows/Properties/PublishProfiles/win10-x86.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + x86 + win-x86 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + False + False + False + True + + + \ No newline at end of file diff --git a/QuickDrawWindows/QuickDraw.csproj b/QuickDrawWindows/QuickDraw.csproj index 6d040ea..8a17ada 100644 --- a/QuickDrawWindows/QuickDraw.csproj +++ b/QuickDrawWindows/QuickDraw.csproj @@ -1,72 +1,135 @@  - WinExe - net6.0-windows10.0.20348.0 - true - true - Always - QuickDraw.ico - Matthew Fraser - MF Digital Media - Gesture drawing app that shows you random images to draw. - Copyright © 2021 MF Digital Media - https://github.com/blendermf/QuickDraw - QuickDraw.png - - https://github.com/blendermf/QuickDraw - Github - Drawing, Gesture, Quick, Poses - en-CA - LICENSE - false - false - QuickDraw.App - 1.0.4 - You can now refresh folder image counts (without re-adding the folder) -Clicking an image during a session now shows it in file explorer. - false - - $(PostBuildEventDependsOn); - PostBuildMacros; - - 7.0 + net10.0-windows10.0.26100.0 + 10.0.18362.0 + QuickDraw + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + win10-$(Platform).pubxml + true + true + None + QuickDraw + en-CA + 1.1.0 + QuickDraw.Program + Matthew Fraser + QuickDraw + Gesture drawing app that shows you random images to draw. + Copyright © 2023 MF Digital Media + https://github.com/blendermf/QuickDraw + QuickDraw256x256.png + README.md + https://github.com/blendermf/QuickDraw + Drawing, Gesture, Quick, Poses + Ported the Windows app to a native .Net app (no longer uses WebView) + LICENSE + Assets\QuickDraw.ico + app.manifest + true + enable + preview + + + + + - - none - false - - + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + - - WebSrc\%(RecursiveDir)%(Filename)%(Extension) - PreserveNewest - - + True - + \ - + + True + \ + + + + + + + True - + \ + + MSBuild:Compile + + + MSBuild:Compile + + + Designer + Never + + + MSBuild:Compile + Never + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + MSBuild:Compile + - - - - - - - - - - - - + + + true + diff --git a/QuickDrawWindows/Services/ActivationService.cs b/QuickDrawWindows/Services/ActivationService.cs new file mode 100644 index 0000000..3fef82e --- /dev/null +++ b/QuickDrawWindows/Services/ActivationService.cs @@ -0,0 +1,57 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using QuickDraw.Activation; +using QuickDraw.Contracts.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QuickDraw.Services; + +public class ActivationService(ActivationHandler defaultHandler, IEnumerable activationHandlers, ISettingsService settingsService) : IActivationService +{ + public async Task ActivateAsync(object activationArgs) + { + await InitializeAsync(); + + App.Window.Activate(); + + await HandleActivationAsync(activationArgs); + + await StartupAsync(); + } + + private async Task HandleActivationAsync(object activationArgs) + { + var activationHandler = activationHandlers.FirstOrDefault(h => h.CanHandle(activationArgs)); + + if (activationHandler != null) + { + await activationHandler.HandleAsync(activationArgs); + } + + if (defaultHandler.CanHandle(activationArgs)) + { + await defaultHandler.HandleAsync(activationArgs); + } + } + + private async Task InitializeAsync() + { + await settingsService.InitializeAsync().ConfigureAwait(false); + await Task.CompletedTask; + } + + private async Task StartupAsync() + { + var presenter = OverlappedPresenter.Create(); + + presenter.PreferredMinimumWidth = 512; + presenter.PreferredMinimumHeight = 312; + App.Window.AppWindow.SetPresenter(presenter); + + await Task.CompletedTask; + } +} diff --git a/QuickDrawWindows/Services/NavigationService.cs b/QuickDrawWindows/Services/NavigationService.cs new file mode 100644 index 0000000..172d223 --- /dev/null +++ b/QuickDrawWindows/Services/NavigationService.cs @@ -0,0 +1,143 @@ +using CommunityToolkit.WinUI.Animations; + +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; +using Microsoft.UI.Xaml.Navigation; +using QuickDraw.Contracts.Services; +using QuickDraw.Contracts.ViewModels; +using QuickDraw.Utilities; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace QuickDraw.Services; + +internal class NavigationService(IPageService pageService) : INavigationService +{ + private readonly IPageService _pageService = pageService; + private object? _lastParameterUsed; + private Frame? _frame; + + public event NavigatedEventHandler? Navigated; + + public Frame? Frame + { + get + { + if (_frame == null) + { + _frame = App.Window.Content as Frame; + RegisterFrameEvents(); + } + + return _frame; + } + + set + { + UnregisterFrameEvents(); + _frame = value; + RegisterFrameEvents(); + } + } + + [MemberNotNullWhen(true, nameof(Frame), nameof(_frame))] + public bool CanGoBack => Frame != null && Frame.CanGoBack; + + private void RegisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated += OnNavigated; + } + } + + private void UnregisterFrameEvents() + { + if (_frame != null) + { + _frame.Navigated -= OnNavigated; + } + } + + public bool GoBack(NavigationTransitionInfo? transitionInfo = null) + { + if (CanGoBack) + { + var vmBeforeNavigation = _frame.GetPageViewModel(); + + if (transitionInfo != null) + { + _frame.GoBack(transitionInfo); + } + else + { + _frame.GoBack(); + } + + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + + return true; + } + + return false; + } + + public bool NavigateTo(string pageKey, object? parameter = null, bool clearNavigation = false, NavigationTransitionInfo? transitionInfo = null) + { + var pageType = _pageService.GetPageType(pageKey); + + if (_frame != null && + (_frame.Content?.GetType() != pageType || (parameter != null && !parameter.Equals(_lastParameterUsed)))) + { + _frame.Tag = clearNavigation; + var vmBeforeNavigation = _frame.GetPageViewModel(); + + bool navigated; + if (transitionInfo != null) + { + navigated = _frame.Navigate(pageType, parameter, transitionInfo); + } + else + { + navigated = _frame.Navigate(pageType, parameter); + } + + if (navigated) + { + _lastParameterUsed = parameter; + if (vmBeforeNavigation is INavigationAware navigationAware) + { + navigationAware.OnNavigatedFrom(); + } + } + + return navigated; + } + + return false; + } + + private void OnNavigated(object sender, NavigationEventArgs e) + { + if (sender is Frame frame) + { + var clearNavigation = (bool)frame.Tag; + if (clearNavigation) + { + frame.BackStack.Clear(); + } + + if (frame.GetPageViewModel() is INavigationAware navigationAware) + { + navigationAware.OnNavigatedTo(e.Parameter); + } + + Navigated?.Invoke(sender, e); + } + } + + public void SetListDataItemForNextConnectedAnimation(object item) => Frame?.SetListDataItemForNextConnectedAnimation(item); +} \ No newline at end of file diff --git a/QuickDrawWindows/Services/PageService.cs b/QuickDrawWindows/Services/PageService.cs new file mode 100644 index 0000000..0718976 --- /dev/null +++ b/QuickDrawWindows/Services/PageService.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +using System; +using System.Collections.Generic; +using Microsoft.UI.Xaml.Controls; + +using QuickDraw.Contracts.Services; +using System.Linq; +using QuickDraw.Views; +using QuickDraw.ViewModels; + +namespace QuickDraw.Services; + +public class PageService : IPageService +{ + private readonly Dictionary _pages = []; + + public PageService() + { + // Configure Pages and their View Models + Configure(); + Configure(); + } + + public Type GetPageType(string key) + { + Type? pageType; + lock (_pages) + { + if (!_pages.TryGetValue(key, out pageType)) + { + throw new ArgumentException($"Page not found: {key}. Did you forget to call PageService.Configure?"); + } + } + + return pageType; + } + + private void Configure() + where VM : ObservableObject + where V : Page + { + lock (_pages) + { + var key = typeof(VM).FullName!; + if (_pages.ContainsKey(key)) + { + throw new ArgumentException($"The key {key} is already configured in PageService"); + } + + var type = typeof(V); + if (_pages.ContainsValue(type)) + { + throw new ArgumentException($"This type is already configured with key {_pages.First(p => p.Value == type).Key}"); + } + + _pages.Add(key, type); + } + } +} \ No newline at end of file diff --git a/QuickDrawWindows/Services/SettingsService.cs b/QuickDrawWindows/Services/SettingsService.cs new file mode 100644 index 0000000..00f51a8 --- /dev/null +++ b/QuickDrawWindows/Services/SettingsService.cs @@ -0,0 +1,93 @@ +using QuickDraw.Contracts.Services; +using QuickDraw.Core.Models; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using ApplicationData = Microsoft.Windows.Storage.ApplicationData; + +namespace QuickDraw.Services; + +class SettingsService : ISettingsService +{ + public Settings? Settings { get; private set; } + + private readonly SemaphoreSlim _ioSemaphore = new(1,1); + private bool _isInitialized = false; + private StorageFolder? _dataFolder; + + public async Task InitializeAsync() + { + if (!_isInitialized) + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appDataFolder = await StorageFolder.GetFolderFromPathAsync(appDataPath); + + _dataFolder = await appDataFolder.CreateFolderAsync("MFDigitalMedia.QuickDraw", CreationCollisionOption.OpenIfExists); + + _isInitialized = true; + + await ReadSettings(); + } + } + + public async Task ReadSettings() + { + await InitializeAsync(); + + await _ioSemaphore.WaitAsync(); + + try + { + var file = await _dataFolder?.CreateFileAsync("settings.json", CreationCollisionOption.OpenIfExists); + + using var stream = await file.OpenStreamForReadAsync(); + Settings = await JsonSerializer.DeserializeAsync(stream); + } + catch (JsonException ex) + { + Debug.WriteLine(ex); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + finally + { + _ioSemaphore.Release(); + } + } + + public async Task WriteSettings() + { + if (Settings == null) + return; + + await InitializeAsync(); + + await _ioSemaphore.WaitAsync(); + try + { + var file = await _dataFolder?.CreateFileAsync("settings.json", CreationCollisionOption.OpenIfExists); + + using var stream = await file.OpenStreamForWriteAsync(); + await JsonSerializer.SerializeAsync(stream, Settings); + } + catch (JsonException ex) + { + Debug.WriteLine(ex); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + finally + { + _ioSemaphore.Release(); + } + } +} diff --git a/QuickDrawWindows/Services/SlideImageService.cs b/QuickDrawWindows/Services/SlideImageService.cs new file mode 100644 index 0000000..cab1fa3 --- /dev/null +++ b/QuickDrawWindows/Services/SlideImageService.cs @@ -0,0 +1,56 @@ +using QuickDraw.Contracts.Services; +using QuickDraw.Core.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace QuickDraw.Services; + +static class TestRandom +{ + private static readonly Random _random = new Random(0); + + public static int GetInt32(Int32 from, Int32 to) + { + return _random.Next(from, to); + } +} + +static class ListExtensions +{ + public static void Shuffle(this IList list) + { + for (var i = 0; i < list.Count - 1; i++) + { + var j = RandomNumberGenerator.GetInt32(i + 1, list.Count); + + (list[i], list[j]) = (list[j], list[i]); + } + } +} + +public class SlideImageService : ISlideImageService +{ + public List Images { get; private set; } = []; + + public TimerEnum SlideDuration { get; set; } + + public async Task LoadImages(IEnumerable folders) + { + try + { + Images = [.. await ImageFolderList.GetImagesForFolders(folders)]; + Images.Shuffle(); + } + catch (Exception ex) + { + // TODO: Properly log + Debug.WriteLine(ex); + } + + return Images.Count; + } +} \ No newline at end of file diff --git a/QuickDrawWindows/Services/TitlebarService.cs b/QuickDrawWindows/Services/TitlebarService.cs new file mode 100644 index 0000000..0a25c5d --- /dev/null +++ b/QuickDrawWindows/Services/TitlebarService.cs @@ -0,0 +1,46 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using QuickDraw.Contracts.Services; +using QuickDraw.Utilities; +using Windows.UI; +namespace QuickDraw.Services; + +public class TitlebarService : ITitlebarService +{ + private MainWindow? _window; + private AppWindowTitleBar? _titlebar; + + private GridLength _leftInset; + private GridLength _rightInset; + + public void Initialize(Window window) + { + _window = window as MainWindow; + var appWindow = _window!.AppWindow; + _titlebar = appWindow.TitleBar; + + _titlebar.ExtendsContentIntoTitleBar = true; + _titlebar.ButtonBackgroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonBackground"]).Color; + _titlebar.ButtonHoverBackgroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonBackgroundPointerOver"]).Color; + _titlebar.ButtonPressedBackgroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonBackgroundPressed"]).Color; + _titlebar.ButtonInactiveBackgroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionBackgroundDisabled"]).Color; + + _titlebar.ButtonForegroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonStroke"]).Color; + _titlebar.ButtonHoverForegroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonStrokePointerOver"]).Color; + _titlebar.ButtonPressedForegroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionButtonStrokePressed"]).Color; + + _titlebar.ButtonInactiveForegroundColor = Color.FromArgb(0xff, 0x66, 0x66, 0x66); //WindowCaptionForegroundDisabled converted to gray with no alpha, for some reason alpha is ignored here + //titlebar.ButtonInactiveForegroundColor = ((SolidColorBrush)Application.Current.Resources["WindowCaptionForegroundDisabled"]).Color; + + var scaleInv = MonitorInfo.GetInvertedScaleAdjustment(_window); + + _leftInset = new(_titlebar.LeftInset * scaleInv, GridUnitType.Pixel); + _rightInset = new(_titlebar.LeftInset * scaleInv, GridUnitType.Pixel); + } + + public AppWindowTitleBar? TitleBar => _titlebar; + + public GridLength LeftInset => _leftInset; + public GridLength RightInset => _rightInset; +} diff --git a/QuickDrawWindows/Utilities/Binding.cs b/QuickDrawWindows/Utilities/Binding.cs new file mode 100644 index 0000000..6ff9d03 --- /dev/null +++ b/QuickDrawWindows/Utilities/Binding.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QuickDraw.Utilities; + +public class Binding +{ + public static bool InvertBool(bool value) + { + return !value; + } +} diff --git a/QuickDrawWindows/Utilities/FolderDialog.cs b/QuickDrawWindows/Utilities/FolderDialog.cs new file mode 100644 index 0000000..efe2ba3 --- /dev/null +++ b/QuickDrawWindows/Utilities/FolderDialog.cs @@ -0,0 +1,312 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuickDraw.Utilities; + +// Disable warning CS0108: 'x' hides inherited member 'y'. Use the new keyword if hiding was intended. +#pragma warning disable 0108 + +internal static class IIDGuid +{ + internal const string IModalWindow = "b4db1657-70d7-485e-8e3e-6fcb5a5c1802"; + internal const string IFileDialog = "42f85136-db7e-439c-85f1-e4075d135fc8"; + internal const string IFileOpenDialog = "d57c7288-d4ad-4768-be02-9d969532d960"; + internal const string IShellItem = "43826d1e-e718-42ee-bc55-a1e261c37bfe"; + internal const string IShellItemArray = "B63EA76D-1F85-456F-A19C-48159EFA858B"; +} + +internal static class CLSIDGuid +{ + internal const string FileOpenDialog = "DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7"; + internal const string ShellItem = "9ac9fbe1-e0a2-4ad6-b4ee-e212013ea917"; +} + +[Flags] +internal enum FileOpenDialogOptions : uint +{ + NoChangeDir = 0x00000008, + PickFolders = 0x00000020, + AllowMultiSelect = 0x00000200, + PathMustExist = 0x00000800, +} + +public enum SIGDN : uint +{ + NORMALDISPLAY = 0, + PARENTRELATIVEPARSING = 0x80018001, + PARENTRELATIVEFORADDRESSBAR = 0x8001c001, + DESKTOPABSOLUTEPARSING = 0x80028000, + PARENTRELATIVEEDITING = 0x80031001, + DESKTOPABSOLUTEEDITING = 0x8004c000, + FILESYSPATH = 0x80058000, + URL = 0x80068000 +} + +[ComImport] +[Guid(IIDGuid.IShellItem)] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +public interface IShellItem +{ + void BindToHandler(nint pbc, + [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, + [MarshalAs(UnmanagedType.LPStruct)] Guid riid, + out nint ppv); + + void GetParent(out IShellItem ppsi); + + void GetDisplayName(SIGDN sigdnName, out nint ppszName); + + void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); + + void Compare(IShellItem psi, uint hint, out int piOrder); +}; + +[ComImport, +Guid(IIDGuid.IShellItemArray), +InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +interface IShellItemArray +{ + // Not supported: IBindCtx + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] nint pbc, [In] ref Guid rbhid, + [In] ref Guid riid, out nint ppvOut); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyStore([In] int Flags, [In] ref Guid riid, out nint ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetPropertyDescriptionList([In] ref PropertyKey keyType, [In] ref Guid riid, out nint ppv); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetAttributes([In] ShellItemArrayAttributeFlags dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCount(out uint pdwNumItems); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + // Not supported: IEnumShellItems (will use GetCount and GetItemAt instead) + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void EnumItems([MarshalAs(UnmanagedType.Interface)] out nint ppenumShellItems); +} + + + +// These two are here for methods we don't use (but need to keep the function order), so they are empty +struct COMDLG_FILTERSPEC { } + +internal interface IFileDialogEvents { } + +internal enum FileDialogAddPosition : uint +{ + +} + +public struct PropertyKey +{ + +} + +public enum ShellItemArrayAttributeFlags +{ + +} + +// End Empty Definitions + + +[ComImport(), +Guid(IIDGuid.IModalWindow), +InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IModalWindow +{ + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), + PreserveSig] + int Show([In] nint parent); +} + +[ComImport(), +Guid(IIDGuid.IFileDialog), +InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IFileDialog : IModalWindow +{ + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), + PreserveSig] + int Show([In] nint parent); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypes([In] uint cFileTypes, [In] COMDLG_FILTERSPEC[] rgFilterSpec); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Advise([In, MarshalAs(UnmanagedType.Interface)] IFileDialogEvents pfde, out uint pdwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOptions([In] FileOpenDialogOptions fos); + + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetOptions(out FileOpenDialogOptions pfos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogAddPosition fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + // Not supported: IShellItemFilter is not defined, converting to IntPtr + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] nint pFilter); +} + +[ComImport(), +Guid(IIDGuid.IFileOpenDialog), +InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IFileOpenDialog : IFileDialog +{ + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), + PreserveSig] + int Show([In] nint parent); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypes([In] uint cFileTypes, [In] COMDLG_FILTERSPEC[] rgFilterSpec); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileTypeIndex([In] uint iFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileTypeIndex(out uint piFileType); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Advise([In, MarshalAs(UnmanagedType.Interface)] IFileDialogEvents pfde, out uint pdwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Unadvise([In] uint dwCookie); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOptions([In] FileOpenDialogOptions fos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetOptions(out FileOpenDialogOptions pfos); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogAddPosition fdap); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void Close([MarshalAs(UnmanagedType.Error)] int hr); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetClientGuid([In] ref Guid guid); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void ClearClientData(); + + // Not supported: IShellItemFilter is not defined, converting to IntPtr + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void SetFilter([MarshalAs(UnmanagedType.Interface)] nint pFilter); + + // Defined by IFileOpenDialog + // --------------------------------------------------------------------------------- + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); + + [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)] + void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); +} + +[ComImport, +ClassInterface(ClassInterfaceType.None), +TypeLibType(TypeLibTypeFlags.FCanCreate), +Guid(CLSIDGuid.FileOpenDialog)] +internal class FileOpenDialogRCW +{ +} + +[ComImport, +Guid(IIDGuid.IFileOpenDialog), +CoClass(typeof(FileOpenDialogRCW))] +internal interface NativeFileOpenDialog : IFileOpenDialog +{ +} \ No newline at end of file diff --git a/QuickDrawWindows/Utilities/FrameExtensions.cs b/QuickDrawWindows/Utilities/FrameExtensions.cs new file mode 100644 index 0000000..0d6c920 --- /dev/null +++ b/QuickDrawWindows/Utilities/FrameExtensions.cs @@ -0,0 +1,9 @@ +using Microsoft.UI.Xaml.Controls; + +namespace QuickDraw.Utilities; + +public static class FrameExtensions +{ + public static object? GetPageViewModel(this Frame frame) => + frame?.Content?.GetType().GetProperty("ViewModel")?.GetValue(frame.Content, null); +} \ No newline at end of file diff --git a/QuickDrawWindows/Utilities/MFNotifyCollectionChangedEventArgs.cs b/QuickDrawWindows/Utilities/MFNotifyCollectionChangedEventArgs.cs new file mode 100644 index 0000000..56fe48a --- /dev/null +++ b/QuickDrawWindows/Utilities/MFNotifyCollectionChangedEventArgs.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace QuickDraw.Utilities; + +public class MFNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs +{ + public bool FromModel = false; + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, bool fromModel) : base(action) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList changedItems, bool fromModel) : base(action, changedItems) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object changedItem, bool fromModel) : base(action, changedItem) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems, bool fromModel) : base(action, newItems, oldItems) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList changedItems, int startingIndex, bool fromModel) : base(action, changedItems, startingIndex) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object changedItem, int index, bool fromModel) : base(action, changedItem, index) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object newItem, object oldItem, bool fromModel) : base(action, newItem, oldItem) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList newItems, IList oldItems, int startingIndex, bool fromModel) : base(action, newItems, oldItems, startingIndex) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList changedItems, int index, int oldIndex, bool fromModel) : base(action, changedItems, index, oldIndex) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object changedItem, int index, int oldIndex, bool fromModel) : base(action, changedItem, index, oldIndex) + { + FromModel = fromModel; + } + + public MFNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, object newItem, object oldItem, int index, bool fromModel) : base(action, newItem, oldItem, index) + { + FromModel = fromModel; + } +} diff --git a/QuickDrawWindows/Utilities/MFObservableCollection.cs b/QuickDrawWindows/Utilities/MFObservableCollection.cs new file mode 100644 index 0000000..e5f443a --- /dev/null +++ b/QuickDrawWindows/Utilities/MFObservableCollection.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +#nullable enable + +namespace QuickDraw.Utilities; + +[Serializable] +[DebuggerDisplay("Count = {Count}")] +public class MFObservableCollection : Collection, INotifyCollectionChanged, INotifyPropertyChanged +{ + private SimpleMonitor? _monitor; // Lazily allocated only when a subclass calls BlockReentrancy() or during serialization. Do not rename (binary serialization) + + [NonSerialized] + private int _blockReentrancyCount; + + /// + /// Initializes a new instance of ObservableCollection that is empty and has default initial capacity. + /// + public MFObservableCollection() + { + } + + /// + /// Initializes a new instance of the ObservableCollection class that contains + /// elements copied from the specified collection and has sufficient capacity + /// to accommodate the number of elements copied. + /// + /// The collection whose elements are copied to the new list. + /// + /// The elements are copied onto the ObservableCollection in the + /// same order they are read by the enumerator of the collection. + /// + /// collection is a null reference + public MFObservableCollection(IEnumerable collection) : base(new List(collection ?? throw new ArgumentNullException(nameof(collection)))) + { + } + + /// + /// Initializes a new instance of the ObservableCollection class + /// that contains elements copied from the specified list + /// + /// The list whose elements are copied to the new list. + /// + /// The elements are copied onto the ObservableCollection in the + /// same order they are read by the enumerator of the list. + /// + /// list is a null reference + public MFObservableCollection(List list) : base(new List(list ?? throw new ArgumentNullException(nameof(list)))) + { + } + + /// + /// Move item at oldIndex to newIndex. + /// + public void Move(int oldIndex, int newIndex) => MoveItem(oldIndex, newIndex); + public void MoveFromModel(int oldIndex, int newIndex) => MoveItemFromModel(oldIndex, newIndex); + + + + /// + /// PropertyChanged event (per ). + /// + event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged + { + add => PropertyChanged += value; + remove => PropertyChanged -= value; + } + + /// + /// Occurs when the collection changes, either by adding or removing an item. + /// + [field: NonSerialized] + public virtual event NotifyCollectionChangedEventHandler? CollectionChanged; + + /// + /// Called by base class Collection<T> when the list is being cleared; + /// raises a CollectionChanged event to any listeners. + /// + protected override void ClearItems() + { + CheckReentrancy(); + base.ClearItems(); + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionReset(); + } + + /// + /// Called by base class Collection<T> when an item is removed from list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void RemoveItem(int index) + { + CheckReentrancy(); + T removedItem = this[index]; + + base.RemoveItem(index); + + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index); + } + + /// + /// Called by base class Collection<T> when an item is removed from list; + /// raises a CollectionChanged event to any listeners. + /// + protected void RemoveItemFromModel(int index) + { + CheckReentrancy(); + T removedItem = this[index]; + + base.RemoveItem(index); + + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index, true); + } + + + /// + /// Called by base class Collection<T> when an item is added to list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void InsertItem(int index, T item) + { + CheckReentrancy(); + base.InsertItem(index, item); + + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); + } + + /// + /// Called by base class Collection<T> when an item is added to list; + /// raises a CollectionChanged event to any listeners. + /// + protected void InsertItemFromModel(int index, T item) + { + CheckReentrancy(); + base.InsertItem(index, item); + + OnCountPropertyChanged(); + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index, true); + } + + /// + /// Called by base class Collection<T> when an item is set in list; + /// raises a CollectionChanged event to any listeners. + /// + protected override void SetItem(int index, T item) + { + CheckReentrancy(); + T originalItem = this[index]; + base.SetItem(index, item); + + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, item, index); + } + + /// + /// Called by base class Collection<T> when an item is set in list; + /// raises a CollectionChanged event to any listeners. + /// + protected void SetItemFromModel(int index, T item) + { + CheckReentrancy(); + T originalItem = this[index]; + base.SetItem(index, item); + + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, item, index, true); + } + + /// + /// Called by base class ObservableCollection<T> when an item is to be moved within the list; + /// raises a CollectionChanged event to any listeners. + /// + protected virtual void MoveItem(int oldIndex, int newIndex) + { + CheckReentrancy(); + + T removedItem = this[oldIndex]; + + base.RemoveItem(oldIndex); + base.InsertItem(newIndex, removedItem); + + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex); + } + + protected virtual void MoveItemFromModel(int oldIndex, int newIndex) + { + CheckReentrancy(); + + T removedItem = this[oldIndex]; + + base.RemoveItem(oldIndex); + base.InsertItem(newIndex, removedItem); + + OnIndexerPropertyChanged(); + OnCollectionChanged(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex, true); + } + + public void SetFromModel(int index, T value) + { + SetItemFromModel(index, value); + } + + public void AddFromModel(T item) + { + InsertItemFromModel(Count, item); + } + + public void InsertFromModel(int index, T item) + { + if ((uint)index > (uint)Count) + { + throw new IndexOutOfRangeException(); + } + + InsertItemFromModel(index, item); + } + + public bool RemoveFromModel(T item) + { + int index = IndexOf(item); + if (index < 0) return false; + RemoveItemFromModel(index); + return true; + } + + public void RemoveAtFromModel(int index) + { + if ((uint)index >= (uint)Count) + { + throw new IndexOutOfRangeException(); + } + + RemoveItemFromModel(index); + } + + /// + /// Raises a PropertyChanged event (per ). + /// + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + /// + /// PropertyChanged event (per ). + /// + [field: NonSerialized] + protected virtual event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Raise CollectionChanged event to any listeners. + /// Properties/methods modifying this ObservableCollection will raise + /// a collection changed event through this virtual method. + /// + /// + /// When overriding this method, either call its base implementation + /// or call to guard against reentrant collection changes. + /// + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + NotifyCollectionChangedEventHandler? handler = CollectionChanged; + if (handler != null) + { + // Not calling BlockReentrancy() here to avoid the SimpleMonitor allocation. + _blockReentrancyCount++; + try + { + handler(this, e); + } + finally + { + _blockReentrancyCount--; + } + } + } + + /// + /// Disallow reentrant attempts to change this collection. E.g. an event handler + /// of the CollectionChanged event is not allowed to make changes to this collection. + /// + /// + /// typical usage is to wrap e.g. a OnCollectionChanged call with a using() scope: + /// + /// using (BlockReentrancy()) + /// { + /// CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, item, index)); + /// } + /// + /// + protected IDisposable BlockReentrancy() + { + _blockReentrancyCount++; + return EnsureMonitorInitialized(); + } + + /// Check and assert for reentrant attempts to change this collection. + /// raised when changing the collection + /// while another collection change is still being notified to other listeners + protected void CheckReentrancy() + { + if (_blockReentrancyCount > 0) + { + // we can allow changes if there's only one listener - the problem + // only arises if reentrant changes make the original event args + // invalid for later listeners. This keeps existing code working + // (e.g. Selector.SelectedItems). + NotifyCollectionChangedEventHandler? handler = CollectionChanged; + if (handler != null && !handler.HasSingleTarget) + throw new InvalidOperationException("ObservableCollectionEx Reentrancy Not Allowed"); + } + } + + /// + /// Helper to raise a PropertyChanged event for the Count property + /// + private void OnCountPropertyChanged() => OnPropertyChanged(EventArgsCache.CountPropertyChanged); + + /// + /// Helper to raise a PropertyChanged event for the Indexer property + /// + private void OnIndexerPropertyChanged() => OnPropertyChanged(EventArgsCache.IndexerPropertyChanged); + + /// + /// Helper to raise CollectionChanged event to any listeners + /// + private void OnCollectionChanged(NotifyCollectionChangedAction action, object? item, int index, bool fromModel = false) + { + if (item == null) return; + OnCollectionChanged(new MFNotifyCollectionChangedEventArgs(action, item, index, fromModel)); + } + + /// + /// Helper to raise CollectionChanged event to any listeners + /// + private void OnCollectionChanged(NotifyCollectionChangedAction action, object? item, int index, int oldIndex, bool fromModel = false) + { + if (item == null) return; + OnCollectionChanged(new MFNotifyCollectionChangedEventArgs(action, item, index, oldIndex, fromModel)); + } + + /// + /// Helper to raise CollectionChanged event to any listeners + /// + private void OnCollectionChanged(NotifyCollectionChangedAction action, object? oldItem, object? newItem, int index, bool fromModel = false) + { + if (oldItem == null || newItem == null) return; + + OnCollectionChanged(new MFNotifyCollectionChangedEventArgs(action, newItem, oldItem, index, fromModel)); + } + + /// + /// Helper to raise CollectionChanged event with action == Reset to any listeners + /// + private void OnCollectionReset() => OnCollectionChanged(EventArgsCache.ResetCollectionChanged); + + private SimpleMonitor EnsureMonitorInitialized() => _monitor ??= new SimpleMonitor(this); + + [OnSerializing] + private void OnSerializing(StreamingContext context) + { + EnsureMonitorInitialized(); + _monitor!._busyCount = _blockReentrancyCount; + } + + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_monitor != null) + { + _blockReentrancyCount = _monitor._busyCount; + _monitor._collection = this; + } + } + + // this class helps prevent reentrant calls + [Serializable] + private sealed class SimpleMonitor : IDisposable + { + internal int _busyCount; // Only used during (de)serialization to maintain compatibility with desktop. Do not rename (binary serialization) + + [NonSerialized] + internal MFObservableCollection _collection; + + public SimpleMonitor(MFObservableCollection collection) + { + Debug.Assert(collection != null); + _collection = collection; + } + + public void Dispose() => _collection._blockReentrancyCount--; + } +} + +internal static class EventArgsCache +{ + internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs("Count"); + internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs("Item[]"); + internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); +} diff --git a/QuickDrawWindows/Utilities/MonitorInfo.cs b/QuickDrawWindows/Utilities/MonitorInfo.cs new file mode 100644 index 0000000..b13c2c1 --- /dev/null +++ b/QuickDrawWindows/Utilities/MonitorInfo.cs @@ -0,0 +1,58 @@ +using Microsoft.UI.Windowing; +using Microsoft.UI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WinRT.Interop; +using Microsoft.UI.Xaml; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using Microsoft.UI.Xaml.Controls; + +namespace QuickDraw.Utilities; + +internal class MonitorInfo +{ + + [DllImport("Shcore.dll", SetLastError = true)] + internal static extern int GetDpiForMonitor(IntPtr hmonitor, Monitor_DPI_Type dpiType, out uint dpiX, out uint dpiY); + + internal enum Monitor_DPI_Type : int + { + MDT_Effective_DPI = 0, + MDT_Angular_DPI = 1, + MDT_Raw_DPI = 2, + MDT_Default = MDT_Effective_DPI + } + + private static uint GetScaleAdjustmentUInt(Window window) + { + IntPtr hWnd = WindowNative.GetWindowHandle(window); + WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd); + DisplayArea displayArea = DisplayArea.GetFromWindowId(wndId, DisplayAreaFallback.Primary); + IntPtr hMonitor = Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId); + + // Get DPI. + int result = GetDpiForMonitor(hMonitor, Monitor_DPI_Type.MDT_Default, out uint dpiX, out uint _); + if (result != 0) + { + throw new Exception("Could not get DPI for monitor."); + } + + return (uint)(((long)dpiX * 100 + (96 >> 1)) / 96); + } + + public static double GetScaleAdjustment(Window window) + { + + return GetScaleAdjustmentUInt(window) / 100.0; + } + + public static double GetInvertedScaleAdjustment(Window window) + { + + return 100.0 / GetScaleAdjustmentUInt(window); + } +} diff --git a/QuickDrawWindows/ViewModels/Base/ViewModelWithToolbarBase.cs b/QuickDrawWindows/ViewModels/Base/ViewModelWithToolbarBase.cs new file mode 100644 index 0000000..255d414 --- /dev/null +++ b/QuickDrawWindows/ViewModels/Base/ViewModelWithToolbarBase.cs @@ -0,0 +1,46 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml; +using QuickDraw.Contracts.Services; +using QuickDraw.Contracts.ViewModels; +using QuickDraw.Views.Base; + +namespace QuickDraw.ViewModels.Base; + +public partial class ViewModelWithTitlebarBase : ObservableObject, IUnloadable, IViewModel +{ + public ITitlebarService TitlebarService; + + public GridLength TitlebarLeftInset => TitlebarService.LeftInset; + public GridLength TitlebarRightInset => TitlebarService.RightInset; + + [ObservableProperty] + public partial bool TitlebarInactive { get; set; } + + public ViewModelWithTitlebarBase(ITitlebarService titlebarService) + { + TitlebarService = titlebarService; + App.Window.Activated += Window_Activated; + } + + public void HandleDragRegionsChanged(object sender, DragRegionsChangedEventArgs args) + { + TitlebarService?.TitleBar?.SetDragRectangles(args.DragRegions); + } + + private void Window_Activated(object sender, WindowActivatedEventArgs args) + { + if (args.WindowActivationState == WindowActivationState.Deactivated) + { + TitlebarInactive = true; + } + else + { + TitlebarInactive = false; + } + } + + public virtual void Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + App.Window.Activated -= Window_Activated; + } +} diff --git a/QuickDrawWindows/ViewModels/ImageFolderViewModel.cs b/QuickDrawWindows/ViewModels/ImageFolderViewModel.cs new file mode 100644 index 0000000..28a705a --- /dev/null +++ b/QuickDrawWindows/ViewModels/ImageFolderViewModel.cs @@ -0,0 +1,24 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using QuickDraw.Core.Models; + +namespace QuickDraw.ViewModels; + +public partial class ImageFolderViewModel : ObservableObject +{ + private ImageFolder _imageFolder; + public string Path { get => _imageFolder.Path; } + + [ObservableProperty] + public partial int ImageCount { get; set; } + + [ObservableProperty] + public partial bool Selected { get; set; } + + public ImageFolderViewModel(ImageFolder imageFolder) + { + _imageFolder = imageFolder; + + ImageCount = imageFolder.ImageCount; + Selected = imageFolder.Selected; + } +} diff --git a/QuickDrawWindows/ViewModels/MainViewModel.cs b/QuickDrawWindows/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..4a009e5 --- /dev/null +++ b/QuickDrawWindows/ViewModels/MainViewModel.cs @@ -0,0 +1,72 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media.Animation; +using QuickDraw.Contracts.Services; +using QuickDraw.Contracts.ViewModels; +using QuickDraw.Core.Models; +using QuickDraw.Services; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace QuickDraw.ViewModels; + +public partial class MainViewModel : Base.ViewModelWithTitlebarBase, INavigationAware +{ + [ObservableProperty] + public partial double TimerSliderValue { get; set; } = TimerEnum.T2m.ToSliderValue(); + + [ObservableProperty] + public partial ObservableCollection ImageFolderCollection { get; set; } + + private INavigationService _navigationService; + private ISettingsService _settingsService; + private ISlideImageService _slideImageService; + + public MainViewModel(INavigationService navigationService, ISettingsService settingsService, ITitlebarService titlebarService, ISlideImageService slideImageService) : base(titlebarService) + { + _navigationService = navigationService; + _settingsService = settingsService; + _slideImageService = slideImageService; + + ImageFolderCollection = new ObservableCollection(_settingsService.Settings!.ImageFolderList.ImageFolders.Select(f => new ImageFolderViewModel(f))); + } + + public IEnumerable GetSelectedFolders() + { + return ImageFolderCollection.Where(f => f.Selected).Select(f => f.Path); + } + + [RelayCommand] + private async Task StartSlideShowAsync() + { + var count = await _slideImageService.LoadImages(GetSelectedFolders()); + + if (count > 0) + { + _slideImageService.SlideDuration = TimerSliderValue.ToTimerEnum(); + _navigationService.NavigateTo(typeof(SlideViewModel).FullName!, null, false, + new SlideNavigationTransitionInfo { Effect = SlideNavigationTransitionEffect.FromRight }); + } + else + { + // TODO: Display the fact there were no images found in the selected folders + } + } + + public async void OnNavigatedTo(object parameter) + { + var delay = TimeSpan.Parse((string)Application.Current.Resources["ControlFastAnimationDuration"]); + + await Task.Delay(delay); + + TitlebarService?.TitleBar?.PreferredHeightOption = TitleBarHeightOption.Standard; + TitlebarService?.TitleBar?.IconShowOptions = IconShowOptions.ShowIconAndSystemMenu; + } + + public void OnNavigatedFrom() { } +} \ No newline at end of file diff --git a/QuickDrawWindows/ViewModels/SlideViewModel.cs b/QuickDrawWindows/ViewModels/SlideViewModel.cs new file mode 100644 index 0000000..4b308f1 --- /dev/null +++ b/QuickDrawWindows/ViewModels/SlideViewModel.cs @@ -0,0 +1,182 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using QuickDraw.Contracts.Services; +using QuickDraw.Contracts.ViewModels; +using QuickDraw.Core.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace QuickDraw.ViewModels; + +static class IntExtensions +{ + public static int Mod(this int value, int denominator) + { + int r = value % denominator; + return r < 0 ? r + denominator : r; + } +} + +public class LoadImageEventArgs(string imagePath) +{ + public string ImagePath = imagePath; +} + +public partial class SlideViewModel(ITitlebarService titlebarService, INavigationService navigationService, ISlideImageService slideImageService) : Base.ViewModelWithTitlebarBase(titlebarService), INavigationAware +{ + public event EventHandler? NextImageHandler; + public event EventHandler? PreviousImageHandler; + + private int _currentImageIndex = 0; + + public string? CurrentImagePath { get; set; } + + public string? NextImagePath { get; set; } + + public string? PreviousImagePath { get; set; } + + private List Images { get => slideImageService.Images; } + + [ObservableProperty] + public partial double Progress { get; set; } + + [RelayCommand] + public void UpdateCurrentImages() + { + CurrentImagePath = slideImageService.Images[_currentImageIndex]; + + NextImagePath = slideImageService.Images[(_currentImageIndex + 1).Mod(Images.Count)]; + + PreviousImagePath = slideImageService.Images[(_currentImageIndex - 1).Mod(Images.Count)]; + } + + [RelayCommand] + public void NextImage(bool fromSlider = false) + { + _ticksElapsed = 0; + + if (!fromSlider) + { + Progress = 0; + } + + if (!Paused && !fromSlider) _slideTimer?.Start(); + + _currentImageIndex = (_currentImageIndex + 1).Mod(Images.Count); + NextImageHandler?.Invoke(this, new(slideImageService.Images[(_currentImageIndex + 1).Mod(Images.Count)])); + + } + + [RelayCommand] + public void PreviousImage() + { + _ticksElapsed = 0; + + Progress = 0; + + if (!Paused) _slideTimer?.Start(); + + _currentImageIndex = (_currentImageIndex - 1).Mod(Images.Count); + PreviousImageHandler?.Invoke(this, new(slideImageService.Images[(_currentImageIndex - 1).Mod(Images.Count)])); + } + + DispatcherQueueTimer? _slideTimer = null; + private uint _ticksElapsed = 0; + + public void StartTimer(DispatcherQueue dispatcherQueue) + { + var timerDurationEnum = slideImageService.SlideDuration; + + if (timerDurationEnum != TimerEnum.NoLimit) + { + _slideTimer = dispatcherQueue.CreateTimer(); + var timerDuration = timerDurationEnum.ToSeconds(); + + Progress = 0; + _slideTimer.IsRepeating = true; + _slideTimer.Interval = new(TimeSpan.TicksPerMillisecond * (long)1000); + _slideTimer.Tick += async (sender, e) => + { + _ticksElapsed += 1; + Progress = 100 * (double)_ticksElapsed / (double)timerDuration; + if (_ticksElapsed >= timerDuration) + { + _ticksElapsed = 0; + + NextImage(true); + + await Task.Delay(100); + Progress = 0; + + } + }; + _slideTimer.Start(); + } + else + { + PauseVisibility = Visibility.Collapsed; + } + } + + public event EventHandler? InvalidateCanvas; + + [ObservableProperty] + public partial bool Grayscale { get; set; } + + [ObservableProperty] + public partial Visibility PauseVisibility { get; set; } + + [RelayCommand] + private void ToggleGrayscale() => Grayscale = !Grayscale; + + [RelayCommand] + public void GoBack() => navigationService.GoBack(); + + [RelayCommand] + public void PausePlay() + { + if (Paused) + { + _slideTimer?.Start(); + Paused = false; + } + else + { + _slideTimer?.Stop(); + Paused = true; + } + } + + [ObservableProperty] + public partial bool Paused { get; private set; } + + partial void OnGrayscaleChanged(bool oldValue, bool newValue) + { + if (oldValue != newValue) + { + InvalidateCanvas?.Invoke(this, EventArgs.Empty); + } + } + + public async void OnNavigatedTo(object parameter) + { + // TODO: toggle pause visibility based on if there is a timer or not + // eg. if SlideTimerDuration == Models.TimerEnum.NoLimit + // Might be a better place, eg if we have the view bind one of their initilization events to the VM + + var delay = TimeSpan.Parse((string)Application.Current.Resources["ControlFastAnimationDuration"]); + + await Task.Delay(delay); + + TitlebarService?.TitleBar?.PreferredHeightOption = TitleBarHeightOption.Tall; + TitlebarService?.TitleBar?.IconShowOptions = IconShowOptions.HideIconAndSystemMenu; + } + + public void OnNavigatedFrom() { } +} diff --git a/QuickDrawWindows/Views/Base/PageBase.cs b/QuickDrawWindows/Views/Base/PageBase.cs new file mode 100644 index 0000000..5edcd34 --- /dev/null +++ b/QuickDrawWindows/Views/Base/PageBase.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; +using QuickDraw.Contracts.ViewModels; + +namespace QuickDraw.Views.Base; + +public partial class PageBase : Page +{ + protected IViewModel ViewModelBase { get; } + + public PageBase(IViewModel viewModel) + { + ViewModelBase = viewModel; + if (viewModel is IUnloadable unloadable) + { + Unloaded += unloadable.Unloaded; + } + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + } +} \ No newline at end of file diff --git a/QuickDrawWindows/Views/Base/TitlebarBaseControl.cs b/QuickDrawWindows/Views/Base/TitlebarBaseControl.cs new file mode 100644 index 0000000..79fcd44 --- /dev/null +++ b/QuickDrawWindows/Views/Base/TitlebarBaseControl.cs @@ -0,0 +1,43 @@ +using DependencyPropertyGenerator; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using QuickDraw.Contracts.Services; +using System; +using System.Diagnostics; +using Windows.Graphics; + +namespace QuickDraw.Views.Base; + +public class DragRegionsChangedEventArgs : EventArgs +{ + public readonly RectInt32[] DragRegions; + + public DragRegionsChangedEventArgs(RectInt32[] dragRegions) + { + DragRegions = dragRegions; + } +} + +[DependencyProperty("Inactive")] +[DependencyProperty("LeftInset")] +[DependencyProperty("RightInset")] +[DependencyProperty("TitlebarService")] +public abstract partial class TitlebarBaseControl : UserControl +{ + public event EventHandler? DragRegionsChanged; + + public TitlebarBaseControl() + { + SizeChanged += OnSizeChanged; + } + + protected void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e) + { + var regions = CalculateDragRegions(); + + DragRegionsChanged?.Invoke(this, new(regions)); + } + + protected abstract RectInt32[] CalculateDragRegions(); +} diff --git a/QuickDrawWindows/Views/ImageFolderListViewControl.xaml b/QuickDrawWindows/Views/ImageFolderListViewControl.xaml new file mode 100644 index 0000000..4903e9f --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderListViewControl.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/ImageFolderListViewControl.xaml.cs b/QuickDrawWindows/Views/ImageFolderListViewControl.xaml.cs new file mode 100644 index 0000000..e6fdef4 --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderListViewControl.xaml.cs @@ -0,0 +1,490 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using QuickDraw.Core.Models; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.InteropServices; +using CommunityToolkit.WinUI; +using System.Threading.Tasks; +using System.Collections.Specialized; +using QuickDraw.Utilities; +using System.Diagnostics; +using WinRT; +using System.Collections.Concurrent; +using Windows.System; +using System.Reflection; +using QuickDraw.Contracts.Services; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace QuickDraw.Views; + +public static class TextBlockExtensions +{ + public static double Width(this string value) + { + var tempTextBlock = new TextBlock { Text = value }; + + tempTextBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity)); + + return tempTextBlock.ActualWidth; + } +} + +public sealed partial class ImageFolderListViewControl : UserControl, INotifyPropertyChanged +{ + public MFObservableCollection? ImageFolderCollection = null; + public event PropertyChangedEventHandler? PropertyChanged; + + GridLength _desiredPathColumnWidth = new(0, GridUnitType.Auto); + public GridLength DesiredPathColumnWidth + { + get => _desiredPathColumnWidth; + set + { + _desiredPathColumnWidth = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DesiredPathColumnWidth))); + } + } + + GridLength _desiredImageCountColumnWidth = new(0, GridUnitType.Auto); + public GridLength DesiredImageCountColumnWidth + { + get => _desiredImageCountColumnWidth; + set + { + _desiredImageCountColumnWidth = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DesiredImageCountColumnWidth))); + } + } + + private Point lastPointerPos = new(); + private List ItemsDraggedSelected = []; + + private HashSet foldersBeingReplaced = []; + + private ImageFolder? maxPathWidthImageFolder; + private ImageFolder? maxImageCountWidthImageFolder; + + private bool startedRefresh = false; + + private ISettingsService _settingsService; + + private Settings? _settings => _settingsService.Settings; + + public ImageFolderListViewControl() + { + _settingsService = App.GetService(); + this.InitializeComponent(); + + + if (_settings != null) + { + ImageFolderCollection = [.. _settings.ImageFolderList.ImageFolders]; + UpdateMaxColumnWidths(); + } + + ImageFolderListView.Loaded += (sender, e) => + { + foreach (var i in ImageFolderCollection?.Index().Where(ft => ft.Item.Selected).Select(ft => ft.Index) ?? []) + { + ImageFolderListView.SelectRange(new(i, 1)); + } + + if (ImageFolderListView.SelectedItems.Count == (ImageFolderCollection?.Count ?? 0)) + { + SelectAllCheckbox.IsChecked = true; + } + else + { + SelectAllCheckbox.IsChecked = false; + } + + ImageFolderListView.SelectionChanged += ImageFolderListView_SelectionChanged; + + }; + + if (ImageFolderCollection != null) + { + ImageFolderCollection.CollectionChanged += (sender, e) => + { + if (!startedRefresh) + { + if (e.NewItems != null) + { + foreach (ImageFolder item in e.NewItems) + { + if (item.IsLoading) + { + startedRefresh = true; + RefreshAll.IsEnabled = false; + } + } + } + } + else + { + if (!ImageFolderCollection.Where(f => f.IsLoading).Any()) + { + RefreshAll.IsEnabled = true; + startedRefresh = false; + } + } + + if ((e as MFNotifyCollectionChangedEventArgs)?.FromModel ?? false) + { + if (e.Action == NotifyCollectionChangedAction.Replace) + { + if (e.NewItems != null) + { + foreach (ImageFolder item in e.NewItems) + { + if (item.Selected) + { + foldersBeingReplaced.Add(item); + } + } + } + } + + return; + } + switch (e.Action) + { + case NotifyCollectionChangedAction.Remove: + if (e.OldItems != null) + { + _settings?.ImageFolderList.ImageFolders.RemoveRange(e.OldStartingIndex, e.OldItems.Count); + } + + break; + + case NotifyCollectionChangedAction.Add: + if (e.NewItems != null) + { + if (e.NewStartingIndex != -1) + { + _settings?.ImageFolderList.ImageFolders.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); + } + else + { + _settings?.ImageFolderList.ImageFolders.AddRange(e.NewItems.OfType()); + } + } + break; + + default: + break; + } + _settingsService?.WriteSettings(); + }; + } + + if (_settings?.ImageFolderList != null) + { + _settings.ImageFolderList.CollectionChanged += (sender, e) => + { + DispatcherQueue.EnqueueAsync(() => + { + var i = 0; + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems != null) + { + i = 0; + foreach (var item in e.NewItems) + { + if (item is ImageFolder folder) + { + ImageFolderCollection?.AddFromModel(folder); + } + i++; + } + UpdateMaxColumnWidths(); + } + + break; + + case NotifyCollectionChangedAction.Replace: + if (e.NewItems != null) + { + i = 0; + foreach (var item in e.NewItems) + { + if (item is ImageFolder folder) + { + ImageFolderCollection?.SetFromModel(e.NewStartingIndex + i, folder); + } + i++; + } + UpdateMaxColumnWidths(); + } + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldItems != null) + { + foreach (var item in e.OldItems) + { + if (item is ImageFolder folder) + ImageFolderCollection?.RemoveFromModel(folder); + } + UpdateMaxColumnWidths(); + } + break; + + default: + break; + } + }); + + _settingsService?.WriteSettings(); + }; + } + + } + + private void UpdateMaxColumnWidths() + { + maxPathWidthImageFolder = ImageFolderCollection?.MaxBy(f => f.Path.Width()); + maxImageCountWidthImageFolder = ImageFolderCollection?.MaxBy(f => f.ImageCount.ToString().Width()); + + UpdateColumnWidths(); + } + + private void UpdateColumnWidths() + { + if (maxPathWidthImageFolder == null || maxImageCountWidthImageFolder == null) return; + + var maxPathWidth = maxPathWidthImageFolder?.Path.Width() ?? 0; + var maxImageCountWidth = maxImageCountWidthImageFolder?.ImageCount.ToString().Width() ?? 0; + + Grid? grid = null; + + if (maxPathWidthImageFolder != null) + { + grid = (ImageFolderListView.ContainerFromItem(maxPathWidthImageFolder) as FrameworkElement)?.FindDescendant(); + } + + ProgressRing? progressRing = grid?.FindDescendant(); + + var ImageCountColumnWidth = Math.Max(maxImageCountWidth, progressRing?.ActualWidth ?? 32) + 20; + + var gridWidth = grid?.ActualWidth ?? 0.0; + var availableWidth = gridWidth - ((grid?.ColumnDefinitions[3].ActualWidth ?? 0.0) + ImageCountColumnWidth + 20); + + var maxPathColumnWidth = maxPathWidth + 1; + var PathColumnWidth = Math.Max(100, Math.Min(availableWidth, maxPathColumnWidth)); + + DesiredPathColumnWidth = new GridLength(PathColumnWidth); + DesiredImageCountColumnWidth = new GridLength(ImageCountColumnWidth); + } + + private void MFImageFolderControl_SizeChanged(object sender, SizeChangedEventArgs args) + { + UpdateColumnWidths(); + } + + private void OpenFolders() + { + IFileOpenDialog? dialog = null; + uint count = 0; + try + { + dialog = new NativeFileOpenDialog(); + dialog.SetOptions( + FileOpenDialogOptions.NoChangeDir + | FileOpenDialogOptions.PickFolders + | FileOpenDialogOptions.AllowMultiSelect + | FileOpenDialogOptions.PathMustExist + ); + _ = dialog.Show(IntPtr.Zero); + + dialog.GetResults(out IShellItemArray shellItemArray); + + if (shellItemArray != null) + { + string? folderpath = null; + shellItemArray.GetCount(out count); + + List paths = new List(); + + for (uint i = 0; i < count; i++) + { + shellItemArray.GetItemAt(i, out IShellItem shellItem); + + if (shellItem != null) + { + shellItem.GetDisplayName(SIGDN.FILESYSPATH, out IntPtr i_result); + folderpath = Marshal.PtrToStringAuto(i_result); + Marshal.FreeCoTaskMem(i_result); + + if (folderpath != null) + { + paths.Add(folderpath); + } + } + } + + /*var settings = (App.Current as App)?.Settings; + settings?.ImageFolderList.AddFolderPaths(paths);*/ + } + } + catch (COMException) + { + // No files or other weird error, do nothing. + } + finally + { + if (dialog != null) + { + _ = Marshal.FinalReleaseComObject(dialog); + } + } + } + + private void AddFoldersButton_Click(object sender, RoutedEventArgs e) + { + + OpenFolders(); + } + + private void ImageFolderListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + bool settingsChanged = false; + + foreach (var item in e.AddedItems.Cast()) + { + var (index, itemInList) = ImageFolderCollection?.Index().FirstOrDefault(t => t.Item.Path == item.Path) ?? (-1, null); + + if (foldersBeingReplaced.Any() && foldersBeingReplaced.Where(f => f.Path == item.Path).Any()) + { + if (itemInList.Selected) + { + ImageFolderListView.SelectedItems.Add(itemInList); + } + + foldersBeingReplaced.RemoveWhere(f => f.Path == item.Path); + } + + else if (index >= 0 && _settings != null) + { + _settings.ImageFolderList.ImageFolders[index].Selected = true; + settingsChanged = true; + } + + } + + foreach (var item in e.RemovedItems.Cast()) + { + var (index, itemInList) = ImageFolderCollection?.Index().FirstOrDefault(t => t.Item.Path == item.Path) ?? (-1, null); + + if (ItemsDraggedSelected.Where(f => f.Path == item.Path).Any()) + { + ImageFolderListView.SelectedItems.Add(itemInList); + continue; + } + + if (foldersBeingReplaced.Any() && foldersBeingReplaced.Where(f => f.Path == item.Path).Any()) + { + if (itemInList.Selected) + { + ImageFolderListView.SelectedItems.Add(itemInList); + } + } + else if (index >= 0 && _settings != null) + { + _settings.ImageFolderList.ImageFolders[index].Selected = false; + settingsChanged = true; + } + } + + if (ImageFolderListView.SelectedItems.Count == ImageFolderCollection?.Count) + { + SelectAllCheckbox.IsChecked = true; + } else + { + SelectAllCheckbox.IsChecked = false; + } + + if (settingsChanged) + { + _settingsService?.WriteSettings(); + } + } + + private void ImageFolderListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + var items = e.Items.Cast(); + + var hovereditems = VisualTreeHelper.FindElementsInHostCoordinates(lastPointerPos, ImageFolderListView); + var hoveritemelem = hovereditems.First(i => i is ListViewItem); + var hoveritem = ImageFolderListView.ItemFromContainer(hoveritemelem as ListViewItem); + + ItemsDraggedSelected.AddRange(items.Where(f => f.Selected)); + + foreach (var item in items) + { + if (item != hoveritem) + { + var itemelem = ImageFolderListView.ContainerFromItem(item) as ListViewItem; + + VisualStateManager.GoToState(itemelem, "DragHidden", true); + } + } + } + + private void ImageFolderListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + var items = args.Items.Cast(); + + foreach (var item in items) + { + var itemelem = ImageFolderListView.ContainerFromItem(item) as ListViewItem; + + VisualStateManager.GoToState(itemelem, "DragVisible", true); + } + ItemsDraggedSelected.Clear(); + } + + private void ImageFolderListView_PointerMoved(object sender, PointerRoutedEventArgs e) + { + lastPointerPos = e.GetCurrentPoint(null).Position; + } + + private void SelectAllCheckbox_Click(object sender, RoutedEventArgs e) + { + if (SelectAllCheckbox.IsChecked ?? false) { ImageFolderListView.SelectAll(); } + else { ImageFolderListView.DeselectAll(); } + + } + + public async Task> GetSelectedFolders() + { + return await DispatcherQueue.EnqueueAsync(() => + { + return ImageFolderListView.SelectedItems.Cast().Select(f => f.Path).ToList(); + }); + } + + private void RefreshAll_Click(object sender, RoutedEventArgs e) + { + _settings?.ImageFolderList.UpdateFolderCounts(); + RefreshAll.IsEnabled = false; + startedRefresh = true; + } +} diff --git a/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml b/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml new file mode 100644 index 0000000..47217cf --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml.cs b/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml.cs new file mode 100644 index 0000000..f70d8b2 --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderListViewControl2.xaml.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using QuickDraw.Core.Models; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.InteropServices; +using CommunityToolkit.WinUI; +using System.Threading.Tasks; +using System.Collections.Specialized; +using QuickDraw.Utilities; +using System.Diagnostics; +using WinRT; +using System.Collections.Concurrent; +using Windows.System; +using System.Reflection; +using System.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace QuickDraw.Views; +public sealed partial class ImageFolderListViewControl2 : UserControl +{ + public object? ItemsSource + { + get { return (object)GetValue(ItemsSourceProperty); } + set { SetValue(ItemsSourceProperty, value); } + } + public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( + nameof(ItemsSource), + typeof(object), + typeof(ImageFolderListViewControl2), + new PropertyMetadata(null) + ); + + public ImageFolderListViewControl2() + { + this.InitializeComponent(); + +/* var settings = (App.Current as App)?.Settings; + + if (settings != null) + { + ImageFolderCollection = [.. settings.ImageFolderList.ImageFolders]; + }*/ + } +} diff --git a/QuickDrawWindows/Views/ImageFolderViewControl.xaml b/QuickDrawWindows/Views/ImageFolderViewControl.xaml new file mode 100644 index 0000000..2ce20b4 --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderViewControl.xaml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/ImageFolderViewControl.xaml.cs b/QuickDrawWindows/Views/ImageFolderViewControl.xaml.cs new file mode 100644 index 0000000..8312fef --- /dev/null +++ b/QuickDrawWindows/Views/ImageFolderViewControl.xaml.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using CommunityToolkit.WinUI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using QuickDraw.Core.Models; +using Windows.System; +using QuickDraw.Utilities; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace QuickDraw.Views; + +public enum ColumnType +{ + Path, + ImageCount, + Grid +} + +public sealed partial class ImageFolderViewControl : UserControl +{ + public ImageFolder Folder + { + get { return (ImageFolder)GetValue(FolderProperty); } + set { SetValue(FolderProperty, value); } + } + + public static readonly DependencyProperty FolderProperty = DependencyProperty.Register( + nameof(Folder), + typeof(ImageFolder), + typeof(ImageFolderViewControl), + new PropertyMetadata(null)); + + public GridLength DesiredPathColumnWidth + { + get { return (GridLength)GetValue(DesiredPathColumnWidthProperty); } + set + { + SetValue(DesiredPathColumnWidthProperty, value); + } + } + + public static readonly DependencyProperty DesiredPathColumnWidthProperty = DependencyProperty.Register( + nameof(DesiredPathColumnWidth), + typeof(GridLength), + typeof(ImageFolderViewControl), + new PropertyMetadata(0.0)); + + public GridLength DesiredImageCountColumnWidth + { + get { return (GridLength)GetValue(DesiredImageCountColumnWidthProperty); } + set { SetValue(DesiredImageCountColumnWidthProperty, value); } + } + + public static readonly DependencyProperty DesiredImageCountColumnWidthProperty = DependencyProperty.Register( + nameof(DesiredImageCountColumnWidth), + typeof(GridLength), + typeof(ImageFolderViewControl), + new PropertyMetadata(0.0)); + + public ImageFolderViewControl() + { + this.InitializeComponent(); + } + + private void Refresh_Click(object sender, RoutedEventArgs e) + { + /*var settings = (App.Current as App)?.Settings; + + settings?.ImageFolderList.UpdateFolderCount(Folder);*/ + } + + private void Folder_Click(object sender, RoutedEventArgs e) + { + var path = Folder.Path; + Task.Run(async () => + { + await Launcher.LaunchFolderPathAsync(path); + }); + + // TODO: probably notify user if this folder no longer exists, maybe offer to delete + } + + private void Delete_Click(object sender, RoutedEventArgs e) + { +/* var settings = (App.Current as App)?.Settings; + settings?.ImageFolderList.RemoveFolder(Folder);*/ + } +} diff --git a/QuickDrawWindows/Views/MainPage.xaml b/QuickDrawWindows/Views/MainPage.xaml new file mode 100644 index 0000000..1536eda --- /dev/null +++ b/QuickDrawWindows/Views/MainPage.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/MainPage.xaml.cs b/QuickDrawWindows/Views/MainPage.xaml.cs new file mode 100644 index 0000000..36cdbf1 --- /dev/null +++ b/QuickDrawWindows/Views/MainPage.xaml.cs @@ -0,0 +1,134 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using QuickDraw.ViewModels; +using QuickDraw.Views.Base; +using Syncfusion.UI.Xaml.Sliders; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace QuickDraw.Views; + +/// +/// Converts the value of the internal slider into text. +/// +/// Internal use only. +internal partial class StringToEnumConverter(Type type) : IValueConverter +{ + public object? Convert(object value, + Type targetType, + object parameter, + string language) + { + var _name = Enum.ToObject(type, (int)Double.Parse((string)value)); + + // Look for a 'Display' attribute. + var _member = type + .GetRuntimeFields() + .FirstOrDefault(x => x.Name == _name.ToString()); + if (_member == null) + { + return _name; + } + + var _attr = _member + .GetCustomAttribute(); + if (_attr == null) + { + return _name; + } + + return _attr.Name; + } + + public object ConvertBack(object value, + Type targetType, + object parameter, + string language) + { + return value; // Never called + } + +} + +/// +/// Converts the value of the internal slider into text. +/// +/// Internal use only. +internal partial class DoubleToEnumConverter(Type type) : IValueConverter +{ + public object? Convert(object value, + Type targetType, + object parameter, + string language) + { + var _name = Enum.ToObject(type, (int)(double)value); + + // Look for a 'Display' attribute. + var _member = type + .GetRuntimeFields() + .FirstOrDefault(x => x.Name == _name.ToString()); + if (_member == null) + { + return _name; + } + + var _attr = _member + .GetCustomAttribute(); + if (_attr == null) + { + return _name; + } + + return _attr.Name; + } + + public object ConvertBack(object value, + Type targetType, + object parameter, + string language) + { + return value; // Never called + } +} + + + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class MainPage : PageBase +{ + public MainViewModel ViewModel + { + get; + } + + public MainPage() : base(App.GetService()) + { + + ViewModel = (MainViewModel)base.ViewModelBase; + + this.InitializeComponent(); + this.Resources.Add("doubleToEnumConverter", new DoubleToEnumConverter(typeof(Core.Models.TimerEnum))); + this.Resources.Add("stringToEnumConverter", new StringToEnumConverter(typeof(Core.Models.TimerEnum))); + } + + private void TimerSlider_ValueChanged(object? sender, SliderValueChangedEventArgs e) + { +/* var settings = (App.Current as App)?.Settings; + + if (settings != null) + { + settings.SlideTimerDuration = e.NewValue.ToTimerEnum(); + settings.WriteSettings(); + }*/ + } +} diff --git a/QuickDrawWindows/Views/MainTitlebarControl.xaml b/QuickDrawWindows/Views/MainTitlebarControl.xaml new file mode 100644 index 0000000..b9f58d6 --- /dev/null +++ b/QuickDrawWindows/Views/MainTitlebarControl.xaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/MainTitlebarControl.xaml.cs b/QuickDrawWindows/Views/MainTitlebarControl.xaml.cs new file mode 100644 index 0000000..a9f668d --- /dev/null +++ b/QuickDrawWindows/Views/MainTitlebarControl.xaml.cs @@ -0,0 +1,31 @@ +using DependencyPropertyGenerator; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using QuickDraw.Utilities; +using QuickDraw.Views.Base; +using Windows.Graphics; + +namespace QuickDraw.Views; + +public sealed partial class MainTitlebarControl : Base.TitlebarBaseControl +{ + public MainTitlebarControl() + { + InitializeComponent(); + } + + protected override RectInt32[] CalculateDragRegions() + { + var scale = this.XamlRoot.RasterizationScale; + + RectInt32 dragRect = new( + (int)(LeftInset.Value * scale), + 0, + (int)(TitleColumn.ActualWidth * scale), + (int)(ActualHeight * scale) + ); + + return [dragRect]; + } +} diff --git a/QuickDrawWindows/Views/SlidePage.xaml b/QuickDrawWindows/Views/SlidePage.xaml new file mode 100644 index 0000000..026694a --- /dev/null +++ b/QuickDrawWindows/Views/SlidePage.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QuickDrawWindows/Views/SlidePage.xaml.cs b/QuickDrawWindows/Views/SlidePage.xaml.cs new file mode 100644 index 0000000..d06804d --- /dev/null +++ b/QuickDrawWindows/Views/SlidePage.xaml.cs @@ -0,0 +1,275 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Effects; +using Microsoft.Graphics.Canvas.UI; +using Microsoft.Graphics.Canvas.UI.Xaml; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; +using QuickDraw.ViewModels; +using QuickDraw.Views.Base; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Windows.Foundation; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace QuickDraw.Views; + +public enum LoadDirection +{ + Backwards, + Forwards +} + +public record LoadData(string Path, LoadDirection Direction); + +public class ChannelQueue +{ + private readonly Channel _channel = Channel.CreateUnbounded(); + + public bool Enqueue(T data) + { + return _channel.Writer.TryWrite(data); + } + + public ValueTask DequeueAsync(CancellationToken token = default) + { + return _channel.Reader.ReadAsync(token); + } + + public ValueTask WaitForNext(CancellationToken token = default) + { + return _channel.Reader.WaitToReadAsync(token); + } +} + +public partial class MFPointerGrid : Grid +{ + public MFPointerGrid() : base() + { + + } + + public void SetCursor(InputCursor? cursor) + { + ProtectedCursor = cursor; + } +} + +public sealed partial class SlidePage : PageBase +{ + // TODO: Implement clicking the image to open it in explorer + private Task? _initImageLoadTask; + + private (string, CanvasVirtualBitmap?) _currentBitmap; + private (string, CanvasVirtualBitmap?) _nextBitmap; + private (string, CanvasVirtualBitmap?) _prevBitmap; + + private readonly CancellationTokenSource _cts = new(); + private readonly ChannelQueue _imageLoadQueue = new(); + + public SlideViewModel ViewModel + { + get; + } + + public SlidePage() : base(App.GetService()) + { + ViewModel = (SlideViewModel)ViewModelBase; + ViewModel.InvalidateCanvas += InvalidateCanvas; + ViewModel.NextImageHandler += (sender, args) => NextImage(args.ImagePath); + ViewModel.PreviousImageHandler += (sender, args) => PrevImage(args.ImagePath); + + CanvasDevice.DebugLevel = CanvasDebugLevel.Information; + + this.InitializeComponent(); + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + ViewModel.StartTimer(DispatcherQueue); + + _ = HandleLoads(_cts.Token); + } + + private async Task HandleLoads(CancellationToken token) + { + while (await _imageLoadQueue.WaitForNext(token)) + { + var data = await _imageLoadQueue.DequeueAsync(token); + + switch (data.Direction) + { + case LoadDirection.Forwards: + await LoadNext(SlideCanvas, data.Path); + break; + case LoadDirection.Backwards: + await LoadPrev(SlideCanvas, data.Path); + break; + } + } + } + + private async Task LoadNext(ICanvasResourceCreator resourceCreator, string imagePath) + { + _prevBitmap.Item2?.Dispose(); + _prevBitmap = _currentBitmap; + _currentBitmap = _nextBitmap; + _nextBitmap = (imagePath, null); + _nextBitmap = (imagePath, await CanvasVirtualBitmap.LoadAsync(resourceCreator, imagePath)); + SlideCanvas?.Invalidate(); + } + + private async Task LoadPrev(ICanvasResourceCreator resourceCreator, string imagePath) + { + _nextBitmap.Item2?.Dispose(); + _nextBitmap = _currentBitmap; + _currentBitmap = _prevBitmap; + _prevBitmap = (imagePath, null); + _prevBitmap = (imagePath, await CanvasVirtualBitmap.LoadAsync(resourceCreator, imagePath)); + SlideCanvas?.Invalidate(); + } + + public void NextImage(string imagePath) + { + _imageLoadQueue.Enqueue(new(imagePath,LoadDirection.Forwards)); + } + + public void PrevImage(string imagePath) + { + _imageLoadQueue.Enqueue(new(imagePath, LoadDirection.Backwards)); + } + + private void InvalidateCanvas(object? sender, EventArgs e) + { + SlideCanvas.Invalidate(); + } + + void SlidePage_Unloaded(object sender, RoutedEventArgs e) + { + _cts.Cancel(); + + _currentBitmap.Item2?.Dispose(); + _prevBitmap.Item2?.Dispose(); + _nextBitmap.Item2?.Dispose(); + + this.SlideCanvas.RemoveFromVisualTree(); + this.SlideCanvas = null; + } + + private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args) + { + if (!IsLoadInProgress()) + { + var bitmap = _currentBitmap.Item2; + + _ = DrawBitmapToView(args.DrawingSession, bitmap, new Size(sender.ActualWidth, sender.ActualHeight), ViewModel.Grayscale); + } + } + + private bool IsLoadInProgress() + { + // No loading task? + if (_initImageLoadTask == null) + return false; + + // Loading task is still running? + if (!_initImageLoadTask.IsCompleted) + return true; + + // Query the load task results and re-throw any exceptions + // so Win2D can see them. This implements requirement #2. + try + { + _initImageLoadTask.Wait(); + } + catch (AggregateException aggregateException) + { + // .NET async tasks wrap all errors in an AggregateException. + // We unpack this so Win2D can directly see any lost device errors. + aggregateException.Handle(exception => { throw exception; }); + } + finally + { + _initImageLoadTask = null; + } + + return false; + } + + private static Rect? DrawBitmapToView(CanvasDrawingSession session, CanvasVirtualBitmap? bitmap, Size canvasSize, bool grayscale) + { + if (bitmap == null) + return null; + + double canvasAspect = canvasSize.Width / canvasSize.Height; + double bitmapAspect = (bitmap?.Bounds.Width ?? 1.0) / (bitmap?.Bounds.Height ?? 1.0); + Size imageRenderSize; + Point imagePos; + + if (bitmapAspect > canvasAspect) + { + imageRenderSize = new Size( + canvasSize.Width, + canvasSize.Width / bitmapAspect + ); + imagePos = new Point(0, (canvasSize.Height - imageRenderSize.Height) / 2); + } + else + { + imageRenderSize = new Size( + canvasSize.Height * bitmapAspect, + canvasSize.Height + ); + imagePos = new Point((canvasSize.Width - imageRenderSize.Width) / 2, 0); + } + + Rect destBounds = new(imagePos, imageRenderSize); + + ICanvasImage? finalImage = grayscale ? new GrayscaleEffect() { Source = bitmap } : bitmap; + + session.DrawImage(finalImage, destBounds, bitmap?.Bounds ?? new Rect()); + + return destBounds; + } + + private void SlideCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args) + { + args.TrackAsyncAction(CreateResourceAsync(sender).AsAsyncAction()); + } + + async Task CreateResourceAsync(CanvasControl sender) + { + // Cancel old load + if (_initImageLoadTask != null) + { + _initImageLoadTask.AsAsyncAction().Cancel(); + try { await _initImageLoadTask; } catch { } + _initImageLoadTask = null; + + } + + _initImageLoadTask = FillImageCacheAsync(sender).ContinueWith(_ => SlideCanvas.Invalidate()); + } + + private async Task FillImageCacheAsync(CanvasControl resourceCreator) + { + ViewModel.UpdateCurrentImagesCommand?.Execute(null); + + var prevBitmapTask = CanvasVirtualBitmap.LoadAsync(resourceCreator, ViewModel.PreviousImagePath!); + var currBitmapTask = CanvasVirtualBitmap.LoadAsync(resourceCreator, ViewModel.CurrentImagePath!); + var nextBitmapTask = CanvasVirtualBitmap.LoadAsync(resourceCreator, ViewModel.NextImagePath!); + + _prevBitmap = (ViewModel.PreviousImagePath!, await prevBitmapTask); + _currentBitmap = (ViewModel.CurrentImagePath!, await currBitmapTask); + _nextBitmap = (ViewModel.NextImagePath!, await nextBitmapTask); + } +} diff --git a/QuickDrawWindows/Views/SlideTitlebarControl.xaml b/QuickDrawWindows/Views/SlideTitlebarControl.xaml new file mode 100644 index 0000000..201a9dd --- /dev/null +++ b/QuickDrawWindows/Views/SlideTitlebarControl.xaml @@ -0,0 +1,174 @@ + + + + + + + 0 + 0 + True + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickDrawWindows/Views/SlideTitlebarControl.xaml.cs b/QuickDrawWindows/Views/SlideTitlebarControl.xaml.cs new file mode 100644 index 0000000..ffefb9f --- /dev/null +++ b/QuickDrawWindows/Views/SlideTitlebarControl.xaml.cs @@ -0,0 +1,52 @@ +using DependencyPropertyGenerator; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using QuickDraw.Contracts.Services; +using QuickDraw.Utilities; +using System.Collections.Generic; +using System.Windows.Input; +using Windows.Graphics; + +namespace QuickDraw.Views; + +[DependencyProperty("Paused", DefaultValue = false)] +[DependencyProperty("PauseVisibility", DefaultValue = Visibility.Visible)] +[DependencyProperty("Progress", DefaultValue = 0)] +[DependencyProperty("NextButtonCommand")] +[DependencyProperty("PreviousButtonCommand")] +[DependencyProperty("GrayscaleButtonCommand")] +[DependencyProperty("PauseButtonCommand")] +[DependencyProperty("BackButtonCommand")] +public sealed partial class SlideTitlebarControl : Base.TitlebarBaseControl +{ + + public SlideTitlebarControl() + { + InitializeComponent(); + } + + protected override RectInt32[] CalculateDragRegions() + { + var scale = this.XamlRoot.RasterizationScale; + + var backWidth = BackColumn.ActualWidth; + var centerLeftWidth = CenterLeftColumn.ActualWidth; + var centerRightWidth = CenterRightColumn.ActualWidth; + + RectInt32 dragRectL = new( + (int)((LeftInset.Value + backWidth) * scale), + 0, + (int)((centerLeftWidth - backWidth - LeftInset.Value) * scale), + (int)(ActualHeight * scale) + ); + + RectInt32 dragRectR = new( + (int)((ActualWidth - centerRightWidth) * scale), + 0, + (int)((centerRightWidth - RightInset.Value) * scale), + (int)(ActualHeight * scale) + ); + + return [dragRectL, dragRectR]; + } +} diff --git a/QuickDrawWindows/app.manifest b/QuickDrawWindows/app.manifest new file mode 100644 index 0000000..b426497 --- /dev/null +++ b/QuickDrawWindows/app.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file