commit 2ac0e3ed2672736de757a54d91750321d6a9158c Author: dan_clark@outlook.com Date: Wed Mar 23 15:44:32 2022 +0000 project diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5184a65 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +TheXamlGuy.TaskbarGroup \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/DataTemplateBuilder.cs b/TheXamlGuy.TaskbarGroup.Core/DataTemplateBuilder.cs new file mode 100644 index 0000000..ec183fc --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/DataTemplateBuilder.cs @@ -0,0 +1,15 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class DataTemplateBuilder : IDataTemplateBuilder + { + private readonly Dictionary items = new(); + + public IDataTemplateCollection DataTemplates => new DataTemplateCollection(items); + + public IDataTemplateBuilder Map() + { + items.Add(typeof(TViewModel), typeof(TView)); + return this; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/DataTemplateCollection.cs b/TheXamlGuy.TaskbarGroup.Core/DataTemplateCollection.cs new file mode 100644 index 0000000..5f52daa --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/DataTemplateCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class DataTemplateCollection : ReadOnlyDictionary, IDataTemplateCollection + { + public DataTemplateCollection(IDictionary dictionary) : base(dictionary) + { + + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/Disposer.cs b/TheXamlGuy.TaskbarGroup.Core/Disposer.cs new file mode 100644 index 0000000..c18992d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/Disposer.cs @@ -0,0 +1,78 @@ +using System.Reactive.Linq; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Collections; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + + public class Disposer : IDisposer + { + private readonly ConditionalWeakTable subjects = new(); + + public void Add(object subject, params object[] objects) + { + var disposables = subjects.GetOrCreateValue(subject); + + foreach (var disposable in objects.OfType()) + { + disposables.Add(disposable); + } + + foreach (var notDisposable in objects.Where(x => x is not IDisposable)) + { + disposables.Add(Disposable.Create(() => MakeNotDisposable(notDisposable))); + } + } + + public void Dispose(object subject) + { + if (subjects.TryGetValue(subject, out CompositeDisposable disposables)) + { + disposables.Dispose(); + } + } + + public void Remove(object subject, IDisposable disposer) + { + var disposables = subjects.GetOrCreateValue(subject); + if (disposer != null) + { + disposables.Remove(disposer); + } + } + + public TDisposable Replace(object subject, IDisposable disposer, TDisposable replacement) where TDisposable : IDisposable + { + var disposables = subjects.GetOrCreateValue(subject); + if (disposer is not null) + { + disposables.Remove(disposer); + } + + disposables.Add(replacement); + return replacement; + } + + private void MakeNotDisposable(object target) + { + if (target is IEnumerable enumerableTarget) + { + foreach (var item in enumerableTarget) + { + MakeNotDisposable(item); + } + } + + if (target is IDisposable disposableTarget) + { + disposableTarget.Dispose(); + } + + if (target is not IDisposable) + { + Dispose(target); + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/EventAggregatorInvoker.cs b/TheXamlGuy.TaskbarGroup.Core/EventAggregatorInvoker.cs new file mode 100644 index 0000000..40cc3f4 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/EventAggregatorInvoker.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class EventAggregatorInvoker : IEventAggregatorInvoker + { + public void Invoke(object target, TMessage message, MethodInfo methodInfo) + { + if (message is null) throw new ArgumentNullException(nameof(message)); + + methodInfo.Invoke(target, new object[] { message }); + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IAsyncMessageHandler.cs b/TheXamlGuy.TaskbarGroup.Core/IAsyncMessageHandler.cs new file mode 100644 index 0000000..f291eb7 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IAsyncMessageHandler.cs @@ -0,0 +1,12 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IAsyncMessageHandler + { + Task Handle(TMessage message, CancellationToken canellationToken = default); + } + + public interface IAsyncMessageHandler + { + Task Handle(TMessage message, CancellationToken cancellationToken = default); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IBindViewModel.cs b/TheXamlGuy.TaskbarGroup.Core/IBindViewModel.cs new file mode 100644 index 0000000..40c3eb8 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IBindViewModel.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IBindViewModel where TViewModel : class + { + public TViewModel ViewModel { get; set; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IDataTemplateBuilder.cs b/TheXamlGuy.TaskbarGroup.Core/IDataTemplateBuilder.cs new file mode 100644 index 0000000..7bb5fdb --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDataTemplateBuilder.cs @@ -0,0 +1,9 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDataTemplateBuilder + { + IDataTemplateCollection DataTemplates { get; } + + IDataTemplateBuilder Map(); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IDataTemplateCollection.cs b/TheXamlGuy.TaskbarGroup.Core/IDataTemplateCollection.cs new file mode 100644 index 0000000..e8a6746 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDataTemplateCollection.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDataTemplateCollection : IReadOnlyDictionary + { + + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimer.cs b/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimer.cs new file mode 100644 index 0000000..5ed64a0 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimer.cs @@ -0,0 +1,9 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDispatcherTimer + { + void Start(); + + void Stop(); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimerFactory.cs b/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimerFactory.cs new file mode 100644 index 0000000..183c2cd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDispatcherTimerFactory.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDispatcherTimerFactory + { + IDispatcherTimer Create(Action actionDelegate, TimeSpan interval); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IDisposer.cs b/TheXamlGuy.TaskbarGroup.Core/IDisposer.cs new file mode 100644 index 0000000..05c172d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDisposer.cs @@ -0,0 +1,13 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDisposer + { + void Add(object subject, params object[] objects); + + void Dispose(object subject); + + void Remove(object subject, IDisposable disposer); + + TDisposable Replace(object subject, IDisposable disposer, TDisposable replacement) where TDisposable : IDisposable; + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IDropTarget.cs b/TheXamlGuy.TaskbarGroup.Core/IDropTarget.cs new file mode 100644 index 0000000..109bd3c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IDropTarget.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IDropTarget + { + void Register(TTarget target); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IEventAggregatorInvoker.cs b/TheXamlGuy.TaskbarGroup.Core/IEventAggregatorInvoker.cs new file mode 100644 index 0000000..c9349cd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IEventAggregatorInvoker.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IEventAggregatorInvoker + { + void Invoke(object target, TMessage message, MethodInfo methodInfo); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IHostingExtensions.cs b/TheXamlGuy.TaskbarGroup.Core/IHostingExtensions.cs new file mode 100644 index 0000000..126024a --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IHostingExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public static class IHostingExtensions + { + public static IHostBuilder ConfigureDataTemplates(this IHostBuilder hostBuilder, Action builderDelegate) + { + hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => + { + var builder = new DataTemplateBuilder(); + builderDelegate?.Invoke(builder); + + serviceCollection.AddSingleton(builder.DataTemplates); + }); + + return hostBuilder; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IInitializable.cs b/TheXamlGuy.TaskbarGroup.Core/IInitializable.cs new file mode 100644 index 0000000..ba762e8 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IInitializable.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IInitializable + { + void Initialize(); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IMediator.cs b/TheXamlGuy.TaskbarGroup.Core/IMediator.cs new file mode 100644 index 0000000..56640cd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IMediator.cs @@ -0,0 +1,17 @@ + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IMediator + { + void Handle(object request, params object[] parameters); + void Handle() where TEvent : new(); + TResponse Handle(params object[] parameters) where TRequest : new(); + TResponse Handle(object request, params object[] parameters); + Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters); + Task HandleAsync(object request, params object[] parameters); + Task HandleAsync() where TEvent : new(); + Task HandleAsync(params object[] parameters) where TRequest : new(); + Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters); + Task HandleAsync(object request, params object[] parameters); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IMessageHandler.cs b/TheXamlGuy.TaskbarGroup.Core/IMessageHandler.cs new file mode 100644 index 0000000..eaf99ec --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IMessageHandler.cs @@ -0,0 +1,12 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IMessageHandler + { + void Handle(TMessage message); + } + + public interface IMessageHandler + { + TReturn Handle(TMessage message); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IMessenger.cs b/TheXamlGuy.TaskbarGroup.Core/IMessenger.cs new file mode 100644 index 0000000..e05ce6c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IMessenger.cs @@ -0,0 +1,14 @@ +using System.Reactive.Concurrency; +using System.Reactive.Subjects; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IMessenger + { + void Send() where TMessage : new(); + + void Send(TMessage message); + + IDisposable Subscribe(Action actionDelegate, IScheduler? scheduler = null, Func? where = null); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IObservableExtensions.cs b/TheXamlGuy.TaskbarGroup.Core/IObservableExtensions.cs new file mode 100644 index 0000000..e084c5a --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IObservableExtensions.cs @@ -0,0 +1,31 @@ +using System.Reactive.Linq; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record FileDropped(); + + public static class IObservableExtensions + { + public static IDisposable WeakSubscribe(this IObservable observable, IEventAggregatorInvoker invoker, Action actionDelegate) + { + var methodInfo = actionDelegate.Method; + var weakReference = new WeakReference(actionDelegate.Target); + IDisposable? subscription = null; + + subscription = observable.Subscribe(item => + { + if (weakReference.Target is object target) + { + invoker.Invoke(target, item, methodInfo); + } + else + { + subscription?.Dispose(); + } + }); + + return subscription; + } + + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IPointerMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/IPointerMonitor.cs new file mode 100644 index 0000000..9a10caa --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IPointerMonitor.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IPointerMonitor : IInitializable, IDisposable + { + + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IServiceCollectionExtensions.cs b/TheXamlGuy.TaskbarGroup.Core/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..7fbce88 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddRequiredCore(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddSingleton() + .AddSingleton(provider => new ServiceFactory(provider.GetService, (type, parameter) => ActivatorUtilities.CreateInstance(provider, type, parameter))) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IServiceFactory.cs b/TheXamlGuy.TaskbarGroup.Core/IServiceFactory.cs new file mode 100644 index 0000000..d875fe5 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IServiceFactory.cs @@ -0,0 +1,10 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IServiceFactory + { + T Create(params object[] parameters); + + T Create(Type type); + T Create(); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ITaskbar.cs b/TheXamlGuy.TaskbarGroup.Core/ITaskbar.cs new file mode 100644 index 0000000..ec095ac --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ITaskbar.cs @@ -0,0 +1,8 @@ + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface ITaskbar + { + TaskbarState GetCurrentState(); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ITaskbarButton.cs b/TheXamlGuy.TaskbarGroup.Core/ITaskbarButton.cs new file mode 100644 index 0000000..2191ccc --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ITaskbarButton.cs @@ -0,0 +1,9 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface ITaskbarButton : IDisposable + { + TaskbarButtonBounds Bounds { get; } + + string Name { get; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/ITaskbarButtonMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/ITaskbarButtonMonitor.cs new file mode 100644 index 0000000..2e5af6f --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ITaskbarButtonMonitor.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface ITaskbarButtonMonitor : IInitializable + { + + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ITaskbarMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/ITaskbarMonitor.cs new file mode 100644 index 0000000..9b3d4a5 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ITaskbarMonitor.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface ITaskbarMonitor : IInitializable + { + + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ITemplateSelector.cs b/TheXamlGuy.TaskbarGroup.Core/ITemplateSelector.cs new file mode 100644 index 0000000..343a947 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ITemplateSelector.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface ITemplateSelector + { + + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/IWndProcMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/IWndProcMonitor.cs new file mode 100644 index 0000000..90f8224 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IWndProcMonitor.cs @@ -0,0 +1,7 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public interface IWndProcMonitor : IInitializable, IDisposable + { + + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/IsExternalInit.cs b/TheXamlGuy.TaskbarGroup.Core/IsExternalInit.cs new file mode 100644 index 0000000..aee96d2 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/IsExternalInit.cs @@ -0,0 +1,13 @@ +namespace System.Runtime.CompilerServices +{ + using System.ComponentModel; + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IsExternalInit + { + + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/MaybeNullWhenAttribute.cs b/TheXamlGuy.TaskbarGroup.Core/MaybeNullWhenAttribute.cs new file mode 100644 index 0000000..87da08d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/MaybeNullWhenAttribute.cs @@ -0,0 +1,24 @@ +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + public sealed class NotNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + public sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + // NOTE: you can find the full list of attributes in this gist: + // https://gist.github.com/Sergio0694/eb988b243dd4a720a66fe369b63e5b08. + // Keeping this one shorter so that the Medium embed doesn't take up too much space. +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/Mediator.cs b/TheXamlGuy.TaskbarGroup.Core/Mediator.cs new file mode 100644 index 0000000..a1a9c00 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/Mediator.cs @@ -0,0 +1,70 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class Mediator : IMediator + { + private readonly IServiceFactory serviceFactory; + + public Mediator(IServiceFactory serviceFactory) + { + this.serviceFactory = serviceFactory; + } + + public void Handle() where TEvent : new() + { + Handle(new TEvent()); + } + + public TResponse Handle(params object[] parameters) where TRequest : new() + { + return Handle(new TRequest(), parameters); + } + + public void Handle(object request, params object[] parameters) + { + GetHandler(typeof(IMessageHandler<>).MakeGenericType(request.GetType()), parameters) + .Handle((dynamic)request); + } + + public TResponse Handle(object request, params object[] parameters) + { + return GetHandler(typeof(IMessageHandler<,>).MakeGenericType(typeof(TResponse), request.GetType()), parameters) + .Handle((dynamic)request); + } + + public Task HandleAsync() where TEvent : new() + { + return HandleAsync(new TEvent()); + } + + public Task HandleAsync(params object[] parameters) where TRequest : new() + { + return HandleAsync(new TRequest(), parameters); + } + public Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters) + { + return GetHandler(typeof(IAsyncMessageHandler<>).MakeGenericType(request.GetType()), parameters) + .Handle((dynamic)request, cancellationToken); + } + + public Task HandleAsync(object request, params object[] parameters) + { + return HandleAsync(request, CancellationToken.None, parameters); + } + + public Task HandleAsync(object request, CancellationToken cancellationToken, params object[] parameters) + { + return GetHandler(typeof(IAsyncMessageHandler<,>).MakeGenericType(typeof(TResponse), request.GetType()), parameters) + .Handle((dynamic)request, cancellationToken); + } + + public Task HandleAsync(object request, params object[] parameters) + { + return HandleAsync(request, CancellationToken.None, parameters); + } + + private dynamic GetHandler(Type type, params object[] parameters) + { + return parameters.Length == 0 ? serviceFactory.Create(type) : serviceFactory.Create(type, parameters); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/Messenger.cs b/TheXamlGuy.TaskbarGroup.Core/Messenger.cs new file mode 100644 index 0000000..9e1a684 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/Messenger.cs @@ -0,0 +1,54 @@ +using System.Reactive.Concurrency; +using System.Collections.Concurrent; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class Messenger : IMessenger + { + public IEventAggregatorInvoker invoker; + private readonly ConcurrentDictionary subjects = new(); + + private IScheduler dispatcher; + + public Messenger(IEventAggregatorInvoker invoker) + { + var synchronizationContext = SynchronizationContext.Current; + if (synchronizationContext is null) throw new NullReferenceException(nameof(synchronizationContext)); + + this.invoker = invoker; + + dispatcher = new SynchronizationContextScheduler(synchronizationContext); + } + public ISubject GetSubject() + { + return (ISubject)subjects.GetOrAdd(typeof(TMessage), type => new BehaviorSubject(default)); + } + + public void Send() where TMessage : new() + { + Send(new TMessage()); + } + + public void Send(TMessage message) + { + GetSubject().OnNext(message); + } + + public IDisposable Subscribe(Action actionDelegate, IScheduler? scheduler = null, Func? where = null) + { + if (scheduler is null) + { + scheduler = Scheduler.Default; + } + + if (where == null) + { + where = x => true; + } + + return GetSubject().AsObservable().Skip(1).Where(where).ObserveOn(scheduler).WeakSubscribe(invoker, actionDelegate); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/NativeMethods.json b/TheXamlGuy.TaskbarGroup.Core/NativeMethods.json new file mode 100644 index 0000000..e1360a4 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "public": true +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/NativeMethods.txt b/TheXamlGuy.TaskbarGroup.Core/NativeMethods.txt new file mode 100644 index 0000000..83ec0d3 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/NativeMethods.txt @@ -0,0 +1,17 @@ +SetWindowsHookEx +GetModuleHandle +CallNextHookEx +GetPhysicalCursorPos +FindWindowEx +FindWindow +GetWindowRect +DestroyWindow +DefWindowProcW +CreateWindowExW +RegisterClassW +GetSystemMetrics +MonitorFromWindow +RegisterWindowMessage +GetDpiForWindow +SetWindowPos +SHCreateShellItemArrayFromDataObject \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ObservableCollectionViewModel.cs b/TheXamlGuy.TaskbarGroup.Core/ObservableCollectionViewModel.cs new file mode 100644 index 0000000..452728b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ObservableCollectionViewModel.cs @@ -0,0 +1,132 @@ +using System.Collections.ObjectModel; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class ObservableCollectionViewModel : + ObservableCollection, IDisposable + where TItemViewModel : class + { + private readonly IDisposer disposer; + private readonly IMessenger messenger; + private readonly IServiceFactory serviceFactory; + + public ObservableCollectionViewModel(IMessenger messenger, + IServiceFactory serviceFactory, + IDisposer disposer) + { + this.messenger = messenger; + this.serviceFactory = serviceFactory; + this.disposer = disposer; + } + + public bool IsInitialized { get; protected set; } + + public void Add() + { + var item = serviceFactory.Create(); + disposer.Add(this, item); + + base.Add(item); + } + + public void Add(object parameter, params object[] parameters) + { + var item = serviceFactory.Create(new[] { parameter }.Concat(parameters)); + disposer.Add(this, item); + + base.Add(item); + } + + public new void Clear() + { + foreach (var item in this) + { + disposer.Dispose(item); + } + + base.Clear(); + } + + public void Dispose() + { + OnDisposing(); + + disposer.Dispose(this); + GC.SuppressFinalize(this); + } + + public void Initialize() + { + if (IsInitialized) + { + return; + } + + IsInitialized = true; + OnInitialize(); + } + + public void Insert(params object[] parameters) where TItem : TItemViewModel + { + var item = serviceFactory.Create(parameters); + disposer.Add(this, item); + + base.Add(item); + } + + public new void Insert(int index, TItemViewModel item) + { + disposer.Add(this, item); + base.Insert(index, item); + } + + public void Insert(int index, params object[] parameters) where TItem : TItemViewModel + { + var item = serviceFactory.Create(parameters); + disposer.Add(this, item); + + base.Insert(index, item); + } + + public void Insert(TItemViewModel item) + { + base.Insert(0, item); + disposer.Add(item); + } + + public new void Remove(TItemViewModel item) + { + disposer.Dispose(item); + base.Remove(item); + } + + protected virtual void OnDisposing() + { + } + + protected virtual void OnInitialize() + { + + } + + protected void Publish(TEvent @event) + { + messenger.Send(@event); + } + + protected void Publish() where TEvent : new() + { + messenger.Send(); + } + + protected void Register(Action action) + { + disposer.Add(this, messenger.Subscribe(action)); + } + + protected void Register(Action action, Func condition) + { + disposer.Add(this, messenger.Subscribe(action, null, condition)); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/ObservableViewModel.cs b/TheXamlGuy.TaskbarGroup.Core/ObservableViewModel.cs new file mode 100644 index 0000000..0f4fbfb --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ObservableViewModel.cs @@ -0,0 +1,68 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + [INotifyPropertyChanged] + public partial class ObservableViewModel : IDisposable + { + private readonly IDisposer disposer; + private readonly IMessenger messenger; + + public ObservableViewModel(IMessenger messenger, + IServiceFactory serviceFactory, + IDisposer disposer) + { + this.messenger = messenger; + this.disposer = disposer; + } + + public bool IsInitialized { get; protected set; } + + public void Dispose() + { + OnDisposing(); + + disposer.Dispose(this); + GC.SuppressFinalize(this); + } + + public void Initialize() + { + if (IsInitialized) + { + return; + } + + IsInitialized = true; + OnInitialize(); + } + protected virtual void OnDisposing() + { + } + + protected virtual void OnInitialize() + { + + } + + protected void Publish(TEvent @event) + { + messenger.Send(@event); + } + + protected void Publish() where TEvent : new() + { + messenger.Send(); + } + + protected void Register(Action action) + { + disposer.Add(this, messenger.Subscribe(action)); + } + + protected void Register(Action action, Func condition) + { + disposer.Add(this, messenger.Subscribe(action, null, condition)); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerButton.cs b/TheXamlGuy.TaskbarGroup.Core/PointerButton.cs new file mode 100644 index 0000000..6f62584 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerButton.cs @@ -0,0 +1,9 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public enum PointerButton + { + Left, + Middle, + Right + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerDrag.cs b/TheXamlGuy.TaskbarGroup.Core/PointerDrag.cs new file mode 100644 index 0000000..0d8271f --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerDrag.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record PointerDrag(PointerLocation Location); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerLocation.cs b/TheXamlGuy.TaskbarGroup.Core/PointerLocation.cs new file mode 100644 index 0000000..2114404 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerLocation.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record PointerLocation(int X, int Y); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/PointerMonitor.cs new file mode 100644 index 0000000..f951524 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerMonitor.cs @@ -0,0 +1,155 @@ +using Windows.Win32; +using Windows.Win32.UI.WindowsAndMessaging; +using Windows.Win32.Foundation; +using System.Diagnostics.CodeAnalysis; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class PointerMonitor : IPointerMonitor + { + private readonly IMessenger messenger; + private bool isDisposed; + private bool isPointerPressed; + private HOOKPROC? mouseEventDelegate; + private UnhookWindowsHookExSafeHandle? mouseHandle; + private bool isPointerDrag; + + public PointerMonitor(IMessenger messenger) + { + this.messenger = messenger; + } + + ~PointerMonitor() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public unsafe void Initialize() + { + InitializeHook(); + } + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + RemoveHook(); + isDisposed = true; + } + } + + private unsafe void InitializeHook() + { + mouseEventDelegate = new HOOKPROC(MouseProc); + mouseHandle = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_MOUSE_LL, mouseEventDelegate, PInvoke.GetModuleHandle("user32.dll"), 0); + } + + private unsafe bool TryGetPointer(out POINT point) + { + fixed (POINT* lpPointLocal = &point) + { + return PInvoke.GetPhysicalCursorPos(lpPointLocal); + } + } + + private bool TryGetPointerLocation([MaybeNullWhen(false)]out PointerLocation location) + { + if (TryGetPointer(out POINT point)) + { + location = new PointerLocation(point.x, point.y); + return true; + + } + + location = null; + return false; + } + + private LRESULT MouseProc(int nCode, WPARAM wParam, LPARAM lParam) + { + if (nCode >= 0) + { + + if (TryGetPointerLocation(out var location)) + { + switch ((uint)wParam.Value) + { + case (uint)WndProcMessages.WM_MOUSEMOVE: + SendPointerMoved(location); + break; + case (uint)WndProcMessages.WM_LBUTTONUP: + SendPointerReleased(location, PointerButton.Left); + break; + case (uint)WndProcMessages.WM_MBUTTONUP: + SendPointerReleased(location, PointerButton.Middle); + break; + case (uint)WndProcMessages.WM_RBUTTONUP: + SendPointerReleased(location, PointerButton.Right); + break; + case (uint)WndProcMessages.WM_LBUTTONDOWN: + SendPointerPressed(location, PointerButton.Left); + break; + case (uint)WndProcMessages.WM_MBUTTONDOWN: + SendPointerPressed(location, PointerButton.Middle); + break; + case (uint)WndProcMessages.WM_RBUTTONDOWN: + SendPointerPressed(location, PointerButton.Right); + break; + } + } + } + + return PInvoke.CallNextHookEx(mouseHandle, nCode, wParam, lParam); + } + + private unsafe void RemoveHook() + { + if (mouseHandle is not null && mouseHandle.DangerousGetHandle() != IntPtr.Zero) + { + PInvoke.UnhookWindowsHookEx((HHOOK)mouseHandle.DangerousGetHandle()); + } + } + + private void SendPointerMoved(PointerLocation location) + { + if (isPointerPressed) + { + if (!isPointerDrag) + { + isPointerDrag = true; + } + + messenger.Send(new PointerDrag(location)); + } + + messenger.Send(new PointerMoved(location)); + } + + private void SendPointerPressed(PointerLocation location, PointerButton button) + { + isPointerPressed = true; + messenger.Send(new PointerPressed(location, button)); + } + + private void SendPointerReleased(PointerLocation location, PointerButton button) + { + if (isPointerPressed) + { + if (isPointerDrag) + { + isPointerDrag = false; + messenger.Send(new PointerDragReleased(location, button)); + } + + isPointerPressed = false; + messenger.Send(new PointerReleased(location, button)); + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerMoved.cs b/TheXamlGuy.TaskbarGroup.Core/PointerMoved.cs new file mode 100644 index 0000000..b1a4421 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerMoved.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record PointerMoved(PointerLocation Location); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerPressed.cs b/TheXamlGuy.TaskbarGroup.Core/PointerPressed.cs new file mode 100644 index 0000000..ef500fc --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerPressed.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record PointerPressed(PointerLocation Location, PointerButton Button = PointerButton.Left); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/PointerReleased.cs b/TheXamlGuy.TaskbarGroup.Core/PointerReleased.cs new file mode 100644 index 0000000..bb93811 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/PointerReleased.cs @@ -0,0 +1,6 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record PointerDragReleased(PointerLocation Location, PointerButton Button = PointerButton.Left); + + public record PointerReleased(PointerLocation Location, PointerButton Button = PointerButton.Left); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/RECTExtensions.cs b/TheXamlGuy.TaskbarGroup.Core/RECTExtensions.cs new file mode 100644 index 0000000..2550cd8 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/RECTExtensions.cs @@ -0,0 +1,14 @@ +using Windows.Foundation; +using Windows.Win32.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + internal static class RECTExtensions + { + internal static Rect ToRect(this RECT rect) + { + if (rect.right - rect.left < 0 || rect.bottom - rect.top < 0) return new Rect(rect.left, rect.top, 0, 0); + return new Rect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/Screen.cs b/TheXamlGuy.TaskbarGroup.Core/Screen.cs new file mode 100644 index 0000000..7480d9c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/Screen.cs @@ -0,0 +1,109 @@ +using System.Runtime.InteropServices; +using Windows.Foundation; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using Windows.Win32.Graphics.Gdi; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class Screen + { + private const int CCHDEVICENAME = 32; + private const int PRIMARY_MONITOR = unchecked((int)0xBAADF00D); + private static readonly bool _multiMonitorSupport; + + private readonly IntPtr _monitorHandle; + + static Screen() + { + _multiMonitorSupport = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS) != 0; + } + + internal Screen(IntPtr monitorHandle) + { + if (!_multiMonitorSupport || monitorHandle == (IntPtr)PRIMARY_MONITOR) + { + Bounds = SystemInformationHelper.VirtualScreen; + Primary = true; + DeviceName = "DISPLAY"; + } + else + { + var monitorData = GetMonitorData(monitorHandle); + + Bounds = new Rect(monitorData.MonitorRect.left, monitorData.MonitorRect.top, monitorData.MonitorRect.right - monitorData.MonitorRect.left, monitorData.MonitorRect.bottom - monitorData.MonitorRect.top); + Primary = (monitorData.Flags & (int)MonitorFlag.MONITOR_DEFAULTTOPRIMARY) != 0; + DeviceName = monitorData.DeviceName; + } + + _monitorHandle = monitorHandle; + } + + private enum MonitorFlag : uint + { + MONITOR_DEFAULTTONULL = 0, + MONITOR_DEFAULTTOPRIMARY = 1, + MONITOR_DEFAULTTONEAREST = 2 + } + + public Rect Bounds { get; } + + public string DeviceName { get; } + + public bool Primary { get; } + + public Rect WorkingArea => GetWorkingArea(); + + public static Screen FromHandle(IntPtr handle) + { + return _multiMonitorSupport ? new Screen(PInvoke.MonitorFromWindow((HWND)handle, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST)) : new Screen((IntPtr)PRIMARY_MONITOR); + } + + public override bool Equals(object? obj) + { + if (obj is not Screen monitor) return false; + return _monitorHandle == monitor._monitorHandle; + } + + public override int GetHashCode() + { + return (int)_monitorHandle; + } + + [DllImport("user32.dll", EntryPoint = "GetMonitorInfo", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool GetMonitorInfoEx(IntPtr hMonitor, ref MonitorData lpmi); + + private MonitorData GetMonitorData(IntPtr monitorHandle) + { + var monitorData = new MonitorData(); + monitorData.Size = Marshal.SizeOf(monitorData); + GetMonitorInfoEx(monitorHandle, ref monitorData); + + return monitorData; + } + + private Rect GetWorkingArea() + { + if (!_multiMonitorSupport || _monitorHandle == (IntPtr)PRIMARY_MONITOR) + { + return SystemInformationHelper.WorkingArea; + } + + var monitorData = GetMonitorData(_monitorHandle); + return new Rect(monitorData.WorkAreaRect.left, monitorData.WorkAreaRect.top, monitorData.WorkAreaRect.right - monitorData.WorkAreaRect.left, monitorData.WorkAreaRect.bottom - monitorData.WorkAreaRect.top); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private struct MonitorData + { + public int Size; + public RECT MonitorRect; + public RECT WorkAreaRect; + public uint Flags; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)] + public string DeviceName; + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/ServiceFactory.cs b/TheXamlGuy.TaskbarGroup.Core/ServiceFactory.cs new file mode 100644 index 0000000..1e34e31 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/ServiceFactory.cs @@ -0,0 +1,30 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class ServiceFactory : IServiceFactory + { + private readonly Func factory; + + private readonly Func factoryWithParameters; + + public ServiceFactory(Func factory, Func factoryWithParameters) + { + this.factory = factory; + this.factoryWithParameters = factoryWithParameters; + } + + public T Create(params object[] parameters) + { + return (T)factoryWithParameters(typeof(T), parameters); + } + + public T Create(Type type) + { + return (T)factory(type); + } + + public T Create() + { + return (T)factory(typeof(T)); + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/SystemInformationHelper.cs b/TheXamlGuy.TaskbarGroup.Core/SystemInformationHelper.cs new file mode 100644 index 0000000..9c87457 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/SystemInformationHelper.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; +using Windows.Foundation; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + internal static class SystemInformationHelper + { + private const int SPI_GETWORKAREA = 48; + + public static Rect VirtualScreen => GetVirtualScreen(); + public static Rect WorkingArea => GetWorkingArea(); + + private static Rect GetVirtualScreen() + { + var size = new Size(PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN), PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN)); + return new Rect(0, 0, size.Width, size.Height); + } + + private static Rect GetWorkingArea() + { + var rect = new RECT(); + + SystemParametersInfo(SPI_GETWORKAREA, 0, ref rect, 0); + return new Rect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool SystemParametersInfo(int nAction, int nParam, ref RECT rc, int nUpdate); + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/Taskbar.cs b/TheXamlGuy.TaskbarGroup.Core/Taskbar.cs new file mode 100644 index 0000000..39b591e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/Taskbar.cs @@ -0,0 +1,80 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class Taskbar : ITaskbar + { + private const string ShellTrayHandleName = "Shell_TrayWnd"; + + private enum AppBarEdge : uint + { + Left = 0, + Top = 1, + Right = 2, + Bottom = 3 + } + + private enum AppBarMessage : uint + { + New = 0x00000000, + Remove = 0x00000001, + QueryPos = 0x00000002, + SetPos = 0x00000003, + GetState = 0x00000004, + GetTaskbarPos = 0x00000005, + Activate = 0x00000006, + GetAutoHideBar = 0x00000007, + SetAutoHideBar = 0x00000008, + WindowPosChanged = 0x00000009, + SetState = 0x0000000A, + } + + public TaskbarState GetCurrentState() + { + var handle = GetSystemTrayHandle(); + var state = new TaskbarState + { + Screen = Screen.FromHandle(handle) + }; + + var appBarData = GetAppBarData(handle); + GetAppBarPosition(ref appBarData); + + state.Rect = appBarData.rect.ToRect(); + state.Placement = (TaskbarPlacement)appBarData.uEdge; + + return state; + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr DefWindowProcW(IntPtr handle, uint msg, IntPtr wParam, IntPtr lParam); + + private static IntPtr GetSystemTrayHandle() => WindowHelper.Find(ShellTrayHandleName); + + [DllImport("shell32.dll", SetLastError = true)] + private static extern IntPtr SHAppBarMessage(AppBarMessage dwMessage, ref AppBarData pData); + + private AppBarData GetAppBarData(IntPtr handle) + { + return new AppBarData + { + cbSize = (uint)Marshal.SizeOf(typeof(AppBarData)), + hWnd = handle + }; + } + + private void GetAppBarPosition(ref AppBarData appBarData) => SHAppBarMessage(AppBarMessage.GetTaskbarPos, ref appBarData); + + [StructLayout(LayoutKind.Sequential)] + private struct AppBarData + { + public uint cbSize; + public IntPtr hWnd; + public uint uCallbackMessage; + public AppBarEdge uEdge; + public RECT rect; + public int lParam; + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButton.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButton.cs new file mode 100644 index 0000000..ebbac49 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButton.cs @@ -0,0 +1,103 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class TaskbarButton : ITaskbarButton + { + private readonly IMessenger messenger; + private readonly IDisposer disposer; + private bool isWithinBounds; + private bool isDrag; + + public TaskbarButton(IMessenger messenger, + IDisposer disposer, + string name, + TaskbarButtonBounds bounds) + { + this.messenger = messenger; + this.disposer = disposer; + Name = name; + Bounds = bounds; + + disposer.Add(this, messenger.Subscribe(OnPointerReleased)); + disposer.Add(this, messenger.Subscribe(OnPointerMoved)); + disposer.Add(this, messenger.Subscribe(OnPointerDrag)); + } + + public TaskbarButtonBounds Bounds { get; internal set; } + + public string Name { get; internal set; } + + public void Dispose() + { + disposer.Dispose(this); + GC.SuppressFinalize(this); + } + + private bool IsWithinBounds(PointerLocation args) + { + if (args.X >= Bounds.X + && args.X <= Bounds.X + Bounds.Width + && args.Y >= Bounds.Y + && args.Y <= Bounds.Y + Bounds.Height) + { + return true; + } + else + { + return false; + } + } + + private void OnPointerDrag(PointerDrag args) + { + if (isWithinBounds) + { + if (isDrag) + { + messenger.Send(new TaskbarButtonDragOver(this)); + } + else + { + messenger.Send(new TaskbarButtonDragEnter(this)); + } + + isDrag = true; + } + else + { + isDrag = false; + } + } + + private void OnPointerMoved(PointerMoved args) + { + if (IsWithinBounds(args.Location)) + { + if (isWithinBounds) + { + return; + } + + isWithinBounds = true; + messenger.Send(new TaskbarButtonEntered(this)); + } + else + { + isDrag = false; + isWithinBounds = false; + } + } + + private void OnPointerReleased(PointerReleased args) + { + if (!isDrag && isWithinBounds) + { + messenger.Send(new TaskbarButtonInvoked(this)); + } + + if (isDrag) + { + isDrag = false; + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonBounds.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonBounds.cs new file mode 100644 index 0000000..abe12b5 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonBounds.cs @@ -0,0 +1,23 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonBounds + { + public TaskbarButtonBounds() + { + + } + + public TaskbarButtonBounds(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + public int X { get; } + public int Y { get; } + public int Width { get; } + public int Height { get; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonCreated.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonCreated.cs new file mode 100644 index 0000000..b44d7b3 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonCreated.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonCreated(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragEnter.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragEnter.cs new file mode 100644 index 0000000..995e4ad --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragEnter.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonDragEnter(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragOver.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragOver.cs new file mode 100644 index 0000000..1a4b16d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonDragOver.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonDragOver(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonEntered.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonEntered.cs new file mode 100644 index 0000000..5a7158b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonEntered.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonEntered(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonInvoked.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonInvoked.cs new file mode 100644 index 0000000..0a98079 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonInvoked.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonInvoked(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonMonitor.cs new file mode 100644 index 0000000..25e5998 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonMonitor.cs @@ -0,0 +1,153 @@ +using Windows.Win32.Foundation; +using UIAutomationClient; +using System.Diagnostics; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class TaskbarButtonMonitor : ITaskbarButtonMonitor + { + private readonly IDispatcherTimer dispatcherTimer; + private readonly IDispatcherTimerFactory dispatcherTimerFactory; + private readonly IServiceFactory serviceFactory; + private readonly IMessenger messenger; + private readonly Dictionary taskbarButtons = new(); + private RECT taskbarBoundsCache; + private IUIAutomationCondition? taskListCondition; + private IUIAutomationElement? taskListElement; + private HWND taskListHandle; + + public TaskbarButtonMonitor(IMessenger messenger, + IDispatcherTimerFactory dispatcherTimerFactory, + IServiceFactory serviceFactory) + { + this.messenger = messenger; + this.dispatcherTimerFactory = dispatcherTimerFactory; + this.serviceFactory = serviceFactory; + + dispatcherTimer = dispatcherTimerFactory.Create(OnDispatcher, TimeSpan.FromMilliseconds(500)); + } + + public void Initialize() + { + var clientUIAutomation = new CUIAutomation(); + taskListCondition = clientUIAutomation.CreateTrueCondition(); + + var trayHandle = WindowHelper.Find("Shell_TrayWnd"); + + var rebarHandle = WindowHelper.Find("ReBarWindow32", trayHandle); + var taskHandle = WindowHelper.Find("MSTaskSwWClass", rebarHandle); + taskListHandle = WindowHelper.Find("MSTaskListWClass", taskHandle); + + taskListElement = clientUIAutomation.ElementFromHandle(taskListHandle); + + if (WindowHelper.TryGetBounds(taskListHandle, out var bounds)) + { + taskbarBoundsCache = bounds; + } + + dispatcherTimer.Start(); + UpdateTaskbarButtons(); + } + + private bool CheckDirtyTaskbarRegion() + { + if (WindowHelper.TryGetBounds(taskListHandle, out var bounds)) + { + var width = taskbarBoundsCache.right - taskbarBoundsCache.left; + var height = taskbarBoundsCache.bottom - taskbarBoundsCache.top; + + var deltaWidth = bounds.right - bounds.left; + var deltaHeight = bounds.bottom - bounds.top; + + if (width != deltaWidth || height != deltaHeight) + { + taskbarBoundsCache = bounds; + return true; + } + } + + return false; + } + + private Dictionary FindTaskbarButtons() + { + var taskElements = taskListElement?.FindAll(TreeScope.TreeScope_Descendants | TreeScope.TreeScope_Children, taskListCondition); + + var buttons = new Dictionary(); + if (taskElements is not null) + { + for (int index = 0; index <= taskElements.Length - 1; index++) + { + var taskUIElement = taskElements.GetElement(index); + var name = taskUIElement.CurrentName; + var rect = taskUIElement.CurrentBoundingRectangle; + + buttons.Add(name, rect); + } + } + + return buttons; + } + + private void OnDispatcher() + { + dispatcherTimer.Stop(); + + if (CheckDirtyTaskbarRegion()) + { + UpdateTaskbarButtons(); + } + + dispatcherTimer.Start(); + } + + private void UpdateTaskbarButtons() + { + if (taskListElement is null) + { + return; + } + + var buttons = FindTaskbarButtons(); + + foreach (var buttonToRemove in taskbarButtons.Where(taskbarButton => !buttons.ContainsKey(taskbarButton.Key))) + { + var key = buttonToRemove.Key; + var button = buttonToRemove.Value; + + Debug.WriteLine($"{key} button removed"); + + taskbarButtons.Remove(key); + messenger.Send(new TaskbarButtonRemoved(button)); + + button.Dispose(); + } + + foreach (var button in buttons) + { + var name = button.Key; + var bounds = button.Value; + + var buttonBounds = new TaskbarButtonBounds(bounds.left, + bounds.top, + bounds.right - bounds.left, + bounds.bottom - bounds.top); + + if (taskbarButtons.TryGetValue(name, out var taskbarButton)) + { + Debug.WriteLine($"{name} button updated"); + + taskbarButtons[name].Bounds = buttonBounds; + messenger.Send(new TaskbarButtonUpdated(taskbarButtons[name])); + } + else + { + Debug.WriteLine($"{name} button added"); + + taskbarButtons.Add(name, serviceFactory.Create(name, buttonBounds)); + messenger.Send(new TaskbarButtonCreated(taskbarButtons[name])); + } + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonRemoved.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonRemoved.cs new file mode 100644 index 0000000..5f9452b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonRemoved.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonRemoved(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonUpdated.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonUpdated.cs new file mode 100644 index 0000000..38a95ba --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarButtonUpdated.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarButtonUpdated(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarChanged.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarChanged.cs new file mode 100644 index 0000000..2da329e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarChanged.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record TaskbarChanged; +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarMonitor.cs new file mode 100644 index 0000000..99e7242 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarMonitor.cs @@ -0,0 +1,30 @@ +using Windows.Win32; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class TaskbarMonitor : ITaskbarMonitor + { + private const int SPI_SETWORKAREA = 0x002F; + + private readonly uint WM_TASKBARCREATED = PInvoke.RegisterWindowMessage("TaskbarCreated"); + private readonly IMessenger messenger; + + public TaskbarMonitor(IMessenger messenger) + { + this.messenger = messenger; + } + + public void Initialize() + { + messenger.Subscribe(OnWndProc); + } + + private void OnWndProc(WndProc args) + { + if (args.Message == WM_TASKBARCREATED || args.Message == (int)WndProcMessages.WM_SETTINGCHANGE && (int)args.WParam == SPI_SETWORKAREA) + { + messenger.Send(); + } + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarPlacement.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarPlacement.cs new file mode 100644 index 0000000..238b6ae --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarPlacement.cs @@ -0,0 +1,10 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public enum TaskbarPlacement + { + Left = 0, + Top = 1, + Right = 2, + Bottom = 3 + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/TaskbarState.cs b/TheXamlGuy.TaskbarGroup.Core/TaskbarState.cs new file mode 100644 index 0000000..85845d8 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TaskbarState.cs @@ -0,0 +1,11 @@ +using Windows.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public struct TaskbarState + { + public TaskbarPlacement Placement; + public Rect Rect; + public Screen Screen; + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/TheXamlGuy.TaskbarGroup.Core.csproj b/TheXamlGuy.TaskbarGroup.Core/TheXamlGuy.TaskbarGroup.Core.csproj new file mode 100644 index 0000000..0468f29 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/TheXamlGuy.TaskbarGroup.Core.csproj @@ -0,0 +1,42 @@ + + + netstandard2.0 + enable + enable + True + 10.0 + x64;x86 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + tlbimp + 0 + 1 + 930299ce-9965-4dec-b0f4-a54848d4b667 + 0 + false + true + + + 0 + 1 + 944de083-8fb8-45cf-bcb7-c477acb2f897 + 0 + tlbimp + false + true + + + diff --git a/TheXamlGuy.TaskbarGroup.Core/WindowHelper.cs b/TheXamlGuy.TaskbarGroup.Core/WindowHelper.cs new file mode 100644 index 0000000..0df9f1d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/WindowHelper.cs @@ -0,0 +1,37 @@ +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class WindowHelper + { + public static void MoveAndResize(HWND handle, int x, int y, int width, int height) + { + PInvoke.SetWindowPos(handle, new HWND(), x, y, width, height, 0); + } + + public static void BringToForeground(HWND handle) + { + if (TryGetBounds(handle, out var bounds)) + { + PInvoke.SetWindowPos(handle, new HWND(), bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top, SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + } + } + + public static HWND Find(string windowName) => PInvoke.FindWindow(windowName, null); + + public static HWND Find(string windowName, HWND parentHandle) + { + return PInvoke.FindWindowEx(parentHandle, new HWND(), windowName, null); + } + + public static unsafe bool TryGetBounds(IntPtr handle, out RECT rect) + { + fixed (RECT* lpRectLocal = &rect) + { + return PInvoke.GetWindowRect(new HWND(handle), lpRectLocal); + } + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/WndProc.cs b/TheXamlGuy.TaskbarGroup.Core/WndProc.cs new file mode 100644 index 0000000..73aff14 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/WndProc.cs @@ -0,0 +1,4 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + public record WndProc(uint Message, uint WParam, uint LParam); +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Core/WndProcMessages.cs b/TheXamlGuy.TaskbarGroup.Core/WndProcMessages.cs new file mode 100644 index 0000000..92f0a48 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/WndProcMessages.cs @@ -0,0 +1,14 @@ +namespace TheXamlGuy.TaskbarGroup.Core +{ + internal enum WndProcMessages + { + WM_LBUTTONUP = 0x0202, + WM_MBUTTONUP = 0x0208, + WM_RBUTTONUP = 0x0205, + WM_MOUSEMOVE = 0x0200, + WM_SETTINGCHANGE = 0x001A, + WM_MBUTTONDOWN = 0x0207, + WM_LBUTTONDOWN = 0x0201, + WM_RBUTTONDOWN = 0x0204 + } +} diff --git a/TheXamlGuy.TaskbarGroup.Core/WndProcMonitor.cs b/TheXamlGuy.TaskbarGroup.Core/WndProcMonitor.cs new file mode 100644 index 0000000..2722b1c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Core/WndProcMonitor.cs @@ -0,0 +1,68 @@ +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace TheXamlGuy.TaskbarGroup.Core +{ + public class WndProcMonitor : IWndProcMonitor + { + private WNDPROC? handler; + private readonly IMessenger messenger; + + public WndProcMonitor(IMessenger messenger) + { + this.messenger = messenger; + } + + public IntPtr Handle { get; private set; } + + public void Dispose() + { + PInvoke.DestroyWindow((HWND)Handle); + } + + private unsafe void InitializeWndProc() + { + var windowName = Guid.NewGuid().ToString(); + handler = Wndproc; + + WNDCLASSW wndProcWindow; + + wndProcWindow.style = 0; + wndProcWindow.lpfnWndProc = handler; + wndProcWindow.cbClsExtra = 0; + wndProcWindow.cbWndExtra = 0; + wndProcWindow.hInstance = new HINSTANCE(); + wndProcWindow.hIcon = new HICON(); + wndProcWindow.hCursor = new HCURSOR(); + wndProcWindow.hbrBackground = new HBRUSH(); + + fixed (char* menuName = "") + { + wndProcWindow.lpszMenuName = new PCWSTR(menuName); + } + + fixed (char* className = windowName) + { + wndProcWindow.lpszClassName = new PCWSTR(className); + } + + PInvoke.RegisterClass(wndProcWindow); + Handle = PInvoke.CreateWindowEx(0, wndProcWindow.lpszClassName, new PCWSTR(), 0, 0, 0, 0, 0, new HWND(), + new HMENU(), + new HINSTANCE()); + } + + private LRESULT Wndproc(HWND param0, uint param1, WPARAM param2, LPARAM param3) + { + messenger.Send(new WndProc(param1, (uint)param2.Value, (uint)param3.Value)); + return PInvoke.DefWindowProc(param0, param1, param2, param3); + } + + public void Initialize() + { + InitializeWndProc(); + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DataTemplateFactory.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DataTemplateFactory.cs new file mode 100644 index 0000000..9161ef9 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DataTemplateFactory.cs @@ -0,0 +1,44 @@ +using System; +using TheXamlGuy.TaskbarGroup.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Markup; +using System.Reflection; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public class DataTemplateFactory : IDataTemplateFactory + { + private readonly IDataTemplateCollection datatemplateCollection; + + public DataTemplateFactory(IDataTemplateCollection datatemplateCollection) + { + this.datatemplateCollection = datatemplateCollection; + } + + public virtual DataTemplate Create(Type dataType) + { + if (dataType is null) throw new ArgumentNullException(nameof(dataType)); + + if (!datatemplateCollection.TryGetValue(dataType, out Type viewType)) + { + var assembly = dataType.GetTypeInfo().Assembly; + viewType = Type.GetType($"{dataType.FullName?.Replace("ViewModel", "View")}, {assembly.FullName}"); + } + + if (viewType is not null) + { + var xaml = $"" + + $"" + + ""; + + return (DataTemplate)XamlReader.Load(xaml); + } + + return new DataTemplate(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DropTarget.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DropTarget.cs new file mode 100644 index 0000000..33a6c74 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/DropTarget.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using Windows.UI.Xaml; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public class DropTarget + { + public void Initialize(UIElement target) + { + target.DragOver += OnDragOver; + target.Drop += OnDrop; + } + + private async void OnDrop(object sender, DragEventArgs args) + { + if (args.DataView.Contains(StandardDataFormats.StorageItems)) + { + var items = await args.DataView.GetStorageItemsAsync(); + if (items.Count > 0) + { + foreach (var storageItem in items) + { + if (storageItem is StorageFile storageFile) + { + if (storageFile.Path is { Length: > 0 }) + { + + } + else + { + var properties = await storageFile.Properties.RetrievePropertiesAsync(new List + { + "System.AppUserModel.ID" + }); + + var appUserModelId = properties["System.AppUserModel.ID"]; + if (appUserModelId is not null) + { + + } + } + } + + if (storageItem is StorageFolder storageFolder) + { + + } + } + } + } + } + + private void OnDragOver(object sender, DragEventArgs args) + { + args.AcceptedOperation = DataPackageOperation.Link; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IBindViewModelExtensions.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IBindViewModelExtensions.cs new file mode 100644 index 0000000..28a00a7 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IBindViewModelExtensions.cs @@ -0,0 +1,37 @@ +using TheXamlGuy.TaskbarGroup.Core; +using Windows.UI.Xaml; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public static class IBindViewModelExtensions + { + public static void Bind(this IBindViewModel view) where TViewModel : class + { + if (view is FrameworkElement frameworkElement) + { + void DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + var viewModelProperty = view.GetType().GetProperty("ViewModel"); + if (viewModelProperty is not null) + { + viewModelProperty.SetMethod.Invoke(sender, new[] { sender.DataContext }); + } + } + + frameworkElement.DataContextChanged += DataContextChanged; + } + } + + public static void Bind(this IBindViewModel view, object viewModel) where TViewModel : class + { + if (view is FrameworkElement frameworkElement) + { + var viewModelProperty = view.GetType().GetProperty("ViewModel"); + if (viewModelProperty is not null) + { + viewModelProperty.SetValue(frameworkElement, viewModel); + } + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IDataTemplateFactory.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IDataTemplateFactory.cs new file mode 100644 index 0000000..33d5c64 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IDataTemplateFactory.cs @@ -0,0 +1,10 @@ +using System; +using Windows.UI.Xaml; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public interface IDataTemplateFactory + { + DataTemplate Create(Type type); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IServiceCollectionExtensions.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..7f6238c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddRequiredFlyoutFoundation(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddSingleton() + .AddSingleton(new DataTemplateCollection(new Dictionary())) + .AddSingleton(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IWindowPrivate.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IWindowPrivate.cs new file mode 100644 index 0000000..d8d99d7 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/IWindowPrivate.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.InteropServices; +using Windows.Foundation; +using Windows.UI.Xaml; + +//PLACEHOLDER - Replace with the *correct* GUID. I haven't found any hint for this, yet. +[ComImport, Guid("15645012-8F3F-5090-B584-DF078FCC509A"), InterfaceType(ComInterfaceType.InterfaceIsIInspectable)] +public interface IAtlasRequestCallback +{ + bool AtlasRequest(uint width, uint height, Windows.Graphics.DirectX.DirectXPixelFormat pixelFormat); +} + +[ComImport, Guid("06636C29-5A17-458D-8EA2-2422D997A922"), InterfaceType(ComInterfaceType.InterfaceIsIInspectable)] +public interface IWindowPrivate +{ + bool TransparentBackground { get; set; } + void Show(); + void Hide(); + void MoveWindow(int x, int y, int width, int height); + void SetAtlasSizeHint(uint width, uint height); + void ReleaseGraphicsDeviceOnSuspend(bool enable); + void SetAtlasRequestCallback(IAtlasRequestCallback callback); + Rect GetWindowContentBoundsForElement(DependencyObject element); +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/AssemblyInfo.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e59e4c9 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TheXamlGuy.TaskbarGroup.Flyout.Foundation")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TheXamlGuy.TaskbarGroup.Flyout.Foundation")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/TheXamlGuy.TaskbarGroup.Flyout.Foundation.rd.xml b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/TheXamlGuy.TaskbarGroup.Flyout.Foundation.rd.xml new file mode 100644 index 0000000..7801bd7 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/Properties/TheXamlGuy.TaskbarGroup.Flyout.Foundation.rd.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TemplateSelector.cs b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TemplateSelector.cs new file mode 100644 index 0000000..20380bb --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TemplateSelector.cs @@ -0,0 +1,31 @@ +using TheXamlGuy.TaskbarGroup.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Foundation +{ + public class TemplateSelector : DataTemplateSelector, ITemplateSelector + { + private readonly DataTemplateFactory dataTemplateFactory; + + public TemplateSelector(DataTemplateFactory dataTemplateFactory) + { + this.dataTemplateFactory = dataTemplateFactory; + } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + if (item is not null) + { + var dataType = item.GetType(); + var dataTemplate = dataTemplateFactory.Create(dataType); + if (dataTemplate is not null) + { + return dataTemplate; + } + } + + return base.SelectTemplateCore(item, container); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TheXamlGuy.TaskbarGroup.Flyout.Foundation.csproj b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TheXamlGuy.TaskbarGroup.Flyout.Foundation.csproj new file mode 100644 index 0000000..c032125 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout.Foundation/TheXamlGuy.TaskbarGroup.Flyout.Foundation.csproj @@ -0,0 +1,160 @@ + + + + + Debug + AnyCPU + {35B035A4-E21B-4379-936B-6DEDA31AF860} + Library + Properties + TheXamlGuy.TaskbarGroup.Flyout.Foundation + TheXamlGuy.TaskbarGroup.Flyout.Foundation + en-US + UAP + 10.0.19041.0 + 10.0.19041.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + enable + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + prompt + 4 + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM64 + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM64 + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + PackageReference + + + + + + + + + + + + + + + 6.0.0 + + + 6.2.13 + + + 2.8.0-prerelease.220118001 + + + + + {40d170f4-f8c1-4fae-8a22-a22bf096bbef} + TheXamlGuy.TaskbarGroup.Core + + + + 14.0 + + + + \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/App.xaml b/TheXamlGuy.TaskbarGroup.Flyout/App.xaml new file mode 100644 index 0000000..3e941fd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/App.xaml @@ -0,0 +1,10 @@ + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Flyout/App.xaml.cs b/TheXamlGuy.TaskbarGroup.Flyout/App.xaml.cs new file mode 100644 index 0000000..92c335e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/App.xaml.cs @@ -0,0 +1,20 @@ +using Microsoft.Toolkit.Win32.UI.XamlHost; +using Windows.ApplicationModel.Activation; +using Windows.UI.Xaml; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public sealed partial class App : XamlApplication + { + public App() + { + Initialize(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + (Window.Current as object as IWindowPrivate).TransparentBackground = true; + base.OnLaunched(args); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/LockScreenLogo.scale-200.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..735f57a Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/LockScreenLogo.scale-200.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/SplashScreen.scale-200.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..023e7f1 Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/SplashScreen.scale-200.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square150x150Logo.scale-200.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..af49fec Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square150x150Logo.scale-200.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.scale-200.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..ce342a2 Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.scale-200.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..f6c02ce Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/StoreLogo.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/StoreLogo.png new file mode 100644 index 0000000..7385b56 Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/StoreLogo.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Assets/Wide310x150Logo.scale-200.png b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..288995b Binary files /dev/null and b/TheXamlGuy.TaskbarGroup.Flyout/Assets/Wide310x150Logo.scale-200.png differ diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.cs b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.cs new file mode 100644 index 0000000..93de67e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.cs @@ -0,0 +1,77 @@ +using System; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media.Animation; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Controls +{ + public class TaskbarButtonFlyout : ContentControl + { + public static readonly DependencyProperty TemplateSettingsProperty = + DependencyProperty.Register(nameof(TemplateSettings), + typeof(TaskbarButtonFlyoutTemplateSettings), typeof(TaskbarButtonFlyout), + new PropertyMetadata(null)); + + private UIElement child; + private Border container; + + public TaskbarButtonFlyout() + { + DefaultStyleKey = typeof(TaskbarButtonFlyout); + TemplateSettings = new TaskbarButtonFlyoutTemplateSettings(); + } + + public event EventHandler Closed; + + public event EventHandler Opened; + + public TaskbarButtonFlyoutTemplateSettings TemplateSettings + { + get => (TaskbarButtonFlyoutTemplateSettings)GetValue(TemplateSettingsProperty); + set => SetValue(TemplateSettingsProperty, value); + } + + public bool IsOpen { get; private set; } + + public void Close() + { + if(container is not null) + { + container.Child = null; + } + } + + protected override void OnApplyTemplate() + { + container = GetTemplateChild("Container") as Border; + if (container != null) + { + child = container.Child; + container.Child = null; + } + } + + public void ShowAt(TaskbarButtonFlyoutPlacement taskbarPlacement) + { + VisualStateManager.GoToState(this, "DefaultPlacement", true); + + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + + var width = child.DesiredSize.Width - 1; + var height = child.DesiredSize.Height - 1; + + TemplateSettings.SetValue(TaskbarButtonFlyoutTemplateSettings.HeightProperty, height); + TemplateSettings.SetValue(TaskbarButtonFlyoutTemplateSettings.WidthProperty, width); + + TemplateSettings.SetValue(TaskbarButtonFlyoutTemplateSettings.NegativeHeightProperty, -height); + TemplateSettings.SetValue(TaskbarButtonFlyoutTemplateSettings.NegativeWidthProperty, -width); + + VisualStateManager.GoToState(this, $"{taskbarPlacement}Placement", true); + + container.Child = child; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.xaml b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.xaml new file mode 100644 index 0000000..e635fcd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyout.xaml @@ -0,0 +1,115 @@ + + + + + + + 1 + + + + + + 1 + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutPlacement.cs b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutPlacement.cs new file mode 100644 index 0000000..683e402 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutPlacement.cs @@ -0,0 +1,10 @@ +namespace TheXamlGuy.TaskbarGroup.Flyout.Controls +{ + public enum TaskbarButtonFlyoutPlacement + { + Left, + Top, + Right, + Bottom, + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutTemplateSettings.cs b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutTemplateSettings.cs new file mode 100644 index 0000000..db6d602 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Controls/TaskbarButtonFlyoutTemplateSettings.cs @@ -0,0 +1,50 @@ +using Windows.UI.Xaml; + +namespace TheXamlGuy.TaskbarGroup.Flyout.Controls +{ + public class TaskbarButtonFlyoutTemplateSettings : DependencyObject + { + public static readonly DependencyProperty HeightProperty = + DependencyProperty.Register(nameof(Height), + typeof(double), typeof(TaskbarButtonFlyoutTemplateSettings), + new PropertyMetadata(0d)); + + public static readonly DependencyProperty NegativeHeightProperty = + DependencyProperty.Register(nameof(NegativeHeight), + typeof(double), typeof(TaskbarButtonFlyoutTemplateSettings), + new PropertyMetadata(0d)); + + public static readonly DependencyProperty NegativeWidthProperty = + DependencyProperty.Register(nameof(NegativeWidth), + typeof(double), typeof(TaskbarButtonFlyoutTemplateSettings), + new PropertyMetadata(0d)); + + public static readonly DependencyProperty WidthProperty = + DependencyProperty.Register(nameof(Width), + typeof(double), typeof(TaskbarButtonFlyoutTemplateSettings), + new PropertyMetadata(0d)); + + public double Height + { + get => (double)GetValue(HeightProperty); + set => SetValue(HeightProperty, value); + } + public double NegativeHeight + { + get => (double)GetValue(NegativeHeightProperty); + set => SetValue(NegativeHeightProperty, value); + } + + public double NegativeWidth + { + get => (double)GetValue(NegativeWidthProperty); + set => SetValue(NegativeWidthProperty, value); + } + + public double Width + { + get => (double)GetValue(WidthProperty); + set => SetValue(WidthProperty, value); + } + } +} \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Package.appxmanifest b/TheXamlGuy.TaskbarGroup.Flyout/Package.appxmanifest new file mode 100644 index 0000000..5aeb49b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Package.appxmanifest @@ -0,0 +1,49 @@ + + + + + + + + + + TheXamlGuy.TaskbarGroup.Flyout + Daniel Clark + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Properties/AssemblyInfo.cs b/TheXamlGuy.TaskbarGroup.Flyout/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f87221c --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("TheXamlGuy.TaskbarGroup.Flyout")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TheXamlGuy.TaskbarGroup.Flyout")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Properties/Default.rd.xml b/TheXamlGuy.TaskbarGroup.Flyout/Properties/Default.rd.xml new file mode 100644 index 0000000..af00722 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Properties/Default.rd.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/TheXamlGuy.TaskbarGroup.Flyout.csproj b/TheXamlGuy.TaskbarGroup.Flyout/TheXamlGuy.TaskbarGroup.Flyout.csproj new file mode 100644 index 0000000..5f559af --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/TheXamlGuy.TaskbarGroup.Flyout.csproj @@ -0,0 +1,159 @@ + + + + + Debug + x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C} + AppContainerExe + Properties + TheXamlGuy.TaskbarGroup.Flyout + TheXamlGuy.TaskbarGroup.Flyout + en-US + UAP + 10.0.19041.0 + 10.0.19041.0 + 14 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + false + 10.0 + enable + + + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x86 + false + prompt + true + + + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x86 + false + prompt + true + true + + + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + x64 + false + prompt + true + + + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + x64 + false + prompt + true + true + + + PackageReference + + + + App.xaml + + + + + + + + TaskbarButtonGroupView.xaml + + + + TaskbarButtonView.xaml + + + + + + Designer + + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + + + 6.2.12 + + + 6.1.3 + + + 2.8.0-prerelease.220118001 + + + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF} + TheXamlGuy.TaskbarGroup.Core + + + {35b035a4-e21b-4379-936b-6deda31af860} + TheXamlGuy.TaskbarGroup.Flyout.Foundation + + + + 14.0 + + + + \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Themes/Generic.xaml b/TheXamlGuy.TaskbarGroup.Flyout/Themes/Generic.xaml new file mode 100644 index 0000000..a3efb7a --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Themes/Generic.xaml @@ -0,0 +1,5 @@ + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupItemViewModel.cs b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupItemViewModel.cs new file mode 100644 index 0000000..3ebda79 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupItemViewModel.cs @@ -0,0 +1,15 @@ +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public class TaskbarButtonGroupItemViewModel : ObservableViewModel + { + public TaskbarButtonGroupItemViewModel(IMessenger messenger, + IServiceFactory serviceFactory, + IDisposer disposer) : base(messenger, serviceFactory, disposer) + { + } + + public string Name { get; set; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml new file mode 100644 index 0000000..5c85f21 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml.cs b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml.cs new file mode 100644 index 0000000..4ef5b85 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupView.xaml.cs @@ -0,0 +1,16 @@ +using TheXamlGuy.TaskbarGroup.Core; +using TheXamlGuy.TaskbarGroup.Flyout.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public sealed partial class TaskbarButtonGroupView : IBindViewModel + { + public TaskbarButtonGroupView() + { + InitializeComponent(); + this.Bind(); + } + + public TaskbarButtonGroupViewModel ViewModel { get; set; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupViewModel.cs b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupViewModel.cs new file mode 100644 index 0000000..a2f689d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonGroupViewModel.cs @@ -0,0 +1,23 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public partial class TaskbarButtonGroupViewModel : ObservableCollectionViewModel + { + public TaskbarButtonGroupViewModel(IMessenger messenger, + IServiceFactory serviceFactory, + IDisposer disposer) : base(messenger, serviceFactory, disposer) + { + Register(OnFileDropped); + } + + [ObservableProperty] + private string name = "hello"; + + private void OnFileDropped(FileDropped args) + { + Add(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml new file mode 100644 index 0000000..5ef39b2 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml @@ -0,0 +1,13 @@ + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml.cs b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml.cs new file mode 100644 index 0000000..639c605 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonView.xaml.cs @@ -0,0 +1,14 @@ +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public sealed partial class TaskbarButtonView : IBindViewModel + { + public TaskbarButtonView() + { + InitializeComponent(); + } + + public TaskbarButtonViewModel ViewModel { get; set; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonViewModel.cs b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonViewModel.cs new file mode 100644 index 0000000..4f5b305 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Flyout/Views/TaskbarButtonViewModel.cs @@ -0,0 +1,22 @@ +using TheXamlGuy.TaskbarGroup.Core; +using TheXamlGuy.TaskbarGroup.Flyout.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Flyout +{ + public class TaskbarButtonViewModel : ObservableViewModel + { + public TaskbarButtonViewModel(IMessenger messenger, + IServiceFactory serviceFactory, + IDisposer disposer, + TemplateSelector templateSelector, + TaskbarButtonGroupViewModel taskbarButtonGroupViewModel) : base(messenger, serviceFactory, disposer) + { + TemplateSelector = templateSelector; + TaskbarButtonGroupViewModel = taskbarButtonGroupViewModel; + } + + public TemplateSelector TemplateSelector { get; } + + public TaskbarButtonGroupViewModel TaskbarButtonGroupViewModel { get; } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/DataTemplateFactory.cs b/TheXamlGuy.TaskbarGroup.Foundation/DataTemplateFactory.cs new file mode 100644 index 0000000..c224c36 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/DataTemplateFactory.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using System.Windows; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public class DataTemplateFactory : IDataTemplateFactory + { + private readonly IDataTemplateCollection datatemplateCollection; + private readonly IServiceFactory serviceFactory; + + public DataTemplateFactory(IDataTemplateCollection datatemplateCollection, + IServiceFactory serviceFactory) + { + this.datatemplateCollection = datatemplateCollection; + this.serviceFactory = serviceFactory; + } + + public virtual DataTemplate? Create(Type dataType) + { + if (dataType is null) throw new ArgumentNullException(nameof(dataType)); + + if (!datatemplateCollection.TryGetValue(dataType, out Type? viewType)) + { + var assembly = dataType.GetTypeInfo().Assembly; + viewType = Type.GetType($"{dataType.FullName?.Replace("ViewModel", "View")}, {assembly.FullName}"); + } + + if (viewType is not null) + { + var view = serviceFactory.Create(viewType); + if (view is not null) + { + return TemplateGenerator.CreateDataTemplate(() => view); + } + } + + return null; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimer.cs b/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimer.cs new file mode 100644 index 0000000..8e1bb15 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimer.cs @@ -0,0 +1,37 @@ +using System; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public class DispatcherTimer : IDispatcherTimer + { + private readonly System.Windows.Threading.DispatcherTimer timer; + private readonly Action actionDelegate; + + public DispatcherTimer(Action actionDelegate, TimeSpan interval) + { + timer = new System.Windows.Threading.DispatcherTimer + { + Interval = interval + }; + + timer.Tick += OnTick; + this.actionDelegate = actionDelegate; + } + + private void OnTick(object? sender, EventArgs args) + { + actionDelegate?.Invoke(); + } + + public void Start() + { + timer.Start(); + } + + public void Stop() + { + timer.Stop(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimerFactory.cs b/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimerFactory.cs new file mode 100644 index 0000000..8f3675f --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/DispatcherTimerFactory.cs @@ -0,0 +1,13 @@ +using System; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public class DispatcherTimerFactory : IDispatcherTimerFactory + { + public IDispatcherTimer Create(Action actionDelegate, TimeSpan interval) + { + return new DispatcherTimer(actionDelegate, interval); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/FileDropTarget.cs b/TheXamlGuy.TaskbarGroup.Foundation/FileDropTarget.cs new file mode 100644 index 0000000..3e05a4b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/FileDropTarget.cs @@ -0,0 +1,82 @@ +using System.Diagnostics; +using System.Windows; +using System.Windows.Shell; +using TheXamlGuy.TaskbarGroup.Core; +using Microsoft.WindowsAPICodePack.Shell; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public class FileDropTarget : IDropTarget + { + private UIElement? target; + private readonly IMessenger messenger; + + public FileDropTarget(IMessenger messenger) + { + this.messenger = messenger; + } + + public void Register(UIElement target) + { + if (this.target is not null) + { + target.DragOver -= OnDragOver; + target.DragEnter -= OnDragEnter; + target.Drop -= OnDrop; + } + + this.target = target; + + target.DragOver += OnDragOver; + target.DragEnter += OnDragEnter; + target.Drop += OnDrop; + } + + private void OnDrop(object sender, DragEventArgs args) + { + String[] fileName = (String[])args.Data.GetFormats(); + + var ddd = ShellObjectCollection.FromDataObject((System.Runtime.InteropServices.ComTypes.IDataObject)args.Data); + + //args.Handled = true; + //var fileName = GetFileName(args.Data); + //messenger.Publish(); + + } + + private string GetFileName(IDataObject data) + { + var filenames = (string[])data.GetData(DataFormats.FileDrop); + return filenames[0]; + } + + private bool IsFileDrop(IDataObject data) + { + return data.GetDataPresent(DataFormats.FileDrop); + } + + private void OnDragEnter(object sender, DragEventArgs args) + { + if (IsFileDrop(args.Data)) + { + args.Effects = DragDropEffects.Link; + } + else + { + args.Effects = DragDropEffects.None; + } + } + + private void OnDragOver(object sender, DragEventArgs args) + { + if (IsFileDrop(args.Data)) + { + args.Effects = DragDropEffects.Link; + } + else + { + args.Effects = DragDropEffects.None; + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/IDataTemplateFactory.cs b/TheXamlGuy.TaskbarGroup.Foundation/IDataTemplateFactory.cs new file mode 100644 index 0000000..2178988 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/IDataTemplateFactory.cs @@ -0,0 +1,9 @@ +using System.Windows; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public interface IDataTemplateFactory + { + DataTemplate? Create(Type type); + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/IServiceCollectionExtensions.cs b/TheXamlGuy.TaskbarGroup.Foundation/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..76b01c2 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/IServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddRequiredFoundation(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddSingleton() + .AddSingleton(new DataTemplateCollection(new Dictionary())) + .AddSingleton() + .AddSingleton() + .AddTransient(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/TemplateGenerator.cs b/TheXamlGuy.TaskbarGroup.Foundation/TemplateGenerator.cs new file mode 100644 index 0000000..a44cac6 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/TemplateGenerator.cs @@ -0,0 +1,20 @@ +using System.Windows; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public static class TemplateGenerator + { + public static DataTemplate CreateDataTemplate(Func factory) + { + var frameworkElementFactory = new FrameworkElementFactory(typeof(TemplateGeneratorControl)); + frameworkElementFactory.SetValue(TemplateGeneratorControl.FactoryProperty, factory); + + var dataTemplate = new DataTemplate(typeof(DependencyObject)) + { + VisualTree = frameworkElementFactory + }; + + return dataTemplate; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/TemplateGeneratorControl.cs b/TheXamlGuy.TaskbarGroup.Foundation/TemplateGeneratorControl.cs new file mode 100644 index 0000000..31daccd --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/TemplateGeneratorControl.cs @@ -0,0 +1,22 @@ +using System.Windows; +using System.Windows.Controls; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + internal sealed class TemplateGeneratorControl : ContentControl + { + internal static readonly DependencyProperty FactoryProperty = + DependencyProperty.Register("Factory", typeof(Func), + typeof(TemplateGeneratorControl), new PropertyMetadata(null, + OnFactoryPropertyChanged)); + + private static void OnFactoryPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) + { + if (dependencyObject is TemplateGeneratorControl sender && args.NewValue is not null) + { + var factory = (Func)args.NewValue; + sender.Content = factory(); + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/TemplateSelector.cs b/TheXamlGuy.TaskbarGroup.Foundation/TemplateSelector.cs new file mode 100644 index 0000000..4178ec7 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/TemplateSelector.cs @@ -0,0 +1,32 @@ +using System.Windows; +using System.Windows.Controls; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public class TemplateSelector : DataTemplateSelector, ITemplateSelector + { + private readonly DataTemplateFactory dataTemplateFactory; + + public TemplateSelector(DataTemplateFactory dataTemplateFactory) + { + this.dataTemplateFactory = dataTemplateFactory; + } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item is not null) + { + var dataType = item.GetType(); + var dataTemplate = dataTemplateFactory.Create(dataType); + if (dataTemplate is not null) + { + dataTemplate.Seal(); + return dataTemplate; + } + } + + return base.SelectTemplate(item, container); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/TheXamlGuy.TaskbarGroup.Foundation.csproj b/TheXamlGuy.TaskbarGroup.Foundation/TheXamlGuy.TaskbarGroup.Foundation.csproj new file mode 100644 index 0000000..b8b501a --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/TheXamlGuy.TaskbarGroup.Foundation.csproj @@ -0,0 +1,19 @@ + + + netcoreapp3.1 + true + enable + enable + 10.0 + x64;x86 + + + + + + + + + + + diff --git a/TheXamlGuy.TaskbarGroup.Foundation/VisualExtensions.cs b/TheXamlGuy.TaskbarGroup.Foundation/VisualExtensions.cs new file mode 100644 index 0000000..d96436e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/VisualExtensions.cs @@ -0,0 +1,26 @@ +using System.Windows; +using System.Windows.Media; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public static class VisualExtensions + { + private static Matrix GetDpi(this Visual visual) + { + var source = PresentationSource.FromVisual(visual); + if (source?.CompositionTarget != null) return (Matrix)source?.CompositionTarget.TransformToDevice; + + return default; + } + + public static double DpiY(this Visual visual) + { + return GetDpi(visual).M22; + } + + public static double DpiX(this Visual visual) + { + return GetDpi(visual).M11; + } + } +} diff --git a/TheXamlGuy.TaskbarGroup.Foundation/WindowExtensions.cs b/TheXamlGuy.TaskbarGroup.Foundation/WindowExtensions.cs new file mode 100644 index 0000000..7624752 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.Foundation/WindowExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Windows; +using System.Windows.Interop; +using TheXamlGuy.TaskbarGroup.Core; +using Windows.Win32.Foundation; + +namespace TheXamlGuy.TaskbarGroup.Foundation +{ + public static class WindowExtensions + { + public static IntPtr GetHandle(this Window window) + { + return new WindowInteropHelper(window).Handle; + } + + public static void MoveAndResize(this Window window, int x, int y, int width, int height) + { + var handle = window.GetHandle(); + WindowHelper.MoveAndResize(new HWND(handle), x, y, width, height); + } + + public static void BringToForeground(this Window window) + { + var handle = window.GetHandle(); + WindowHelper.BringToForeground(new HWND(handle)); + } + + } +} diff --git a/TheXamlGuy.TaskbarGroup.sln b/TheXamlGuy.TaskbarGroup.sln new file mode 100644 index 0000000..b30e907 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup.sln @@ -0,0 +1,147 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TheXamlGuy.TaskbarGroup.Foundation", "TheXamlGuy.TaskbarGroup.Foundation\TheXamlGuy.TaskbarGroup.Foundation.csproj", "{C632FABF-A191-406F-962B-CBE1F9B18A12}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TheXamlGuy.TaskbarGroup.Core", "TheXamlGuy.TaskbarGroup.Core\TheXamlGuy.TaskbarGroup.Core.csproj", "{40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TheXamlGuy.TaskbarGroup.Flyout", "TheXamlGuy.TaskbarGroup.Flyout\TheXamlGuy.TaskbarGroup.Flyout.csproj", "{4B56828E-30D0-4EAF-89BC-480480989E6C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TheXamlGuy.TaskbarGroup", "TheXamlGuy.TaskbarGroup\TheXamlGuy.TaskbarGroup.csproj", "{1F70F55F-C04F-4FF7-8824-EF06AC4595BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TheXamlGuy.TaskbarGroup.Flyout.Foundation", "TheXamlGuy.TaskbarGroup.Flyout.Foundation\TheXamlGuy.TaskbarGroup.Flyout.Foundation.csproj", "{35B035A4-E21B-4379-936B-6DEDA31AF860}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|ARM.Build.0 = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|ARM64.Build.0 = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|x64.ActiveCfg = Debug|x64 + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|x64.Build.0 = Debug|x64 + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|x86.ActiveCfg = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Debug|x86.Build.0 = Debug|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|Any CPU.Build.0 = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|ARM.ActiveCfg = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|ARM.Build.0 = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|ARM64.ActiveCfg = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|ARM64.Build.0 = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|x64.ActiveCfg = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|x64.Build.0 = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|x86.ActiveCfg = Release|Any CPU + {C632FABF-A191-406F-962B-CBE1F9B18A12}.Release|x86.Build.0 = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|ARM.ActiveCfg = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|ARM.Build.0 = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|ARM64.Build.0 = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|x64.ActiveCfg = Debug|x64 + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|x64.Build.0 = Debug|x64 + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|x86.ActiveCfg = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Debug|x86.Build.0 = Debug|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|Any CPU.Build.0 = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|ARM.ActiveCfg = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|ARM.Build.0 = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|ARM64.ActiveCfg = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|ARM64.Build.0 = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|x64.ActiveCfg = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|x64.Build.0 = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|x86.ActiveCfg = Release|Any CPU + {40D170F4-F8C1-4FAE-8A22-A22BF096BBEF}.Release|x86.Build.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM.Build.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM.Deploy.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM64.Build.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|ARM64.Deploy.0 = Debug|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x64.ActiveCfg = Debug|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x64.Build.0 = Debug|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x64.Deploy.0 = Debug|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x86.ActiveCfg = Debug|x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x86.Build.0 = Debug|x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Debug|x86.Deploy.0 = Debug|x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|Any CPU.Build.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|Any CPU.Deploy.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM.ActiveCfg = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM.Build.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM.Deploy.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM64.ActiveCfg = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM64.Build.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|ARM64.Deploy.0 = Release|Any CPU + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x64.ActiveCfg = Release|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x64.Build.0 = Release|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x64.Deploy.0 = Release|x64 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x86.ActiveCfg = Release|x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x86.Build.0 = Release|x86 + {4B56828E-30D0-4EAF-89BC-480480989E6C}.Release|x86.Deploy.0 = Release|x86 + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|ARM.ActiveCfg = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|ARM.Build.0 = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|ARM64.Build.0 = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|x64.ActiveCfg = Debug|x64 + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|x64.Build.0 = Debug|x64 + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Debug|x86.Build.0 = Debug|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|Any CPU.Build.0 = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|ARM.ActiveCfg = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|ARM.Build.0 = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|ARM64.ActiveCfg = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|ARM64.Build.0 = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|x64.ActiveCfg = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|x64.Build.0 = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|x86.ActiveCfg = Release|Any CPU + {1F70F55F-C04F-4FF7-8824-EF06AC4595BA}.Release|x86.Build.0 = Release|Any CPU + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|ARM.ActiveCfg = Debug|ARM + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|ARM.Build.0 = Debug|ARM + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|ARM64.Build.0 = Debug|ARM64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|x64.ActiveCfg = Debug|x64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|x64.Build.0 = Debug|x64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|x86.ActiveCfg = Debug|x86 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Debug|x86.Build.0 = Debug|x86 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|Any CPU.Build.0 = Release|Any CPU + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|ARM.ActiveCfg = Release|ARM + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|ARM.Build.0 = Release|ARM + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|ARM64.ActiveCfg = Release|ARM64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|ARM64.Build.0 = Release|ARM64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|x64.ActiveCfg = Release|x64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|x64.Build.0 = Release|x64 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|x86.ActiveCfg = Release|x86 + {35B035A4-E21B-4379-936B-6DEDA31AF860}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {579B0371-17D6-45C2-99DD-E630CE50AF90} + EndGlobalSection +EndGlobal diff --git a/TheXamlGuy.TaskbarGroup/App.xaml b/TheXamlGuy.TaskbarGroup/App.xaml new file mode 100644 index 0000000..c593785 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/App.xaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/TheXamlGuy.TaskbarGroup/App.xaml.cs b/TheXamlGuy.TaskbarGroup/App.xaml.cs new file mode 100644 index 0000000..f87366d --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/App.xaml.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.IO; +using System.Reflection; +using System.Windows; +using TheXamlGuy.TaskbarGroup.Core; +using TheXamlGuy.TaskbarGroup.Flyout; +using TheXamlGuy.TaskbarGroup.Flyout.Foundation; +using TheXamlGuy.TaskbarGroup.Foundation; + +namespace TheXamlGuy.TaskbarGroup +{ + public partial class App : Application + { + private IHost? host; + + protected override async void OnStartup(StartupEventArgs args) + { + var appLocation = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + + host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(config => + { + config.SetBasePath(appLocation); + }) + .ConfigureServices(ConfigureServices) + .Build(); + + await host.StartAsync(); + } + + private void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + services.AddHostedService() + .AddRequiredCore() + .AddRequiredFoundation() + .AddRequiredFlyoutFoundation() + .AddTransient, TaskbarButtonFlyoutActivationHandler>() + .AddSingleton() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/AssemblyInfo.cs b/TheXamlGuy.TaskbarGroup/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/AssemblyInfo.cs @@ -0,0 +1,10 @@ +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/TheXamlGuy.TaskbarGroup/LifeCycles/ApplicationHost.cs b/TheXamlGuy.TaskbarGroup/LifeCycles/ApplicationHost.cs new file mode 100644 index 0000000..2543693 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/LifeCycles/ApplicationHost.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup +{ + public sealed class ApplicationHost : IHostedService + { + private readonly IEnumerable initializables; + private bool isInitialized; + private readonly TaskbarButtonFlyoutWindow flyoutWindow; + + public ApplicationHost(IEnumerable initializables, + TaskbarButtonFlyoutWindow flyoutWindow) + { + this.initializables = initializables; + this.flyoutWindow = flyoutWindow; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await InitializeAsync(); + await StartupAsync(); + + isInitialized = true; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + } + + private async Task InitializeAsync() + { + if (!isInitialized) + { + foreach (var initializable in initializables) + { + initializable.Initialize(); + } + + await Task.CompletedTask; + } + } + + private async Task StartupAsync() + { + if (!isInitialized) + { + flyoutWindow.Show(); + + await Task.CompletedTask; + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivation.cs b/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivation.cs new file mode 100644 index 0000000..4e40bab --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivation.cs @@ -0,0 +1,6 @@ +using TheXamlGuy.TaskbarGroup.Core; + +namespace TheXamlGuy.TaskbarGroup +{ + public record TaskbarButtonFlyoutActivation(TaskbarButton Button); +} diff --git a/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivationHandler.cs b/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivationHandler.cs new file mode 100644 index 0000000..7f3b4c5 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/LifeCycles/TaskbarButtonFlyoutActivationHandler.cs @@ -0,0 +1,70 @@ +using TheXamlGuy.TaskbarGroup.Core; +using TheXamlGuy.TaskbarGroup.Flyout; +using TheXamlGuy.TaskbarGroup.Flyout.Controls; +using TheXamlGuy.TaskbarGroup.Flyout.Foundation; +using TheXamlGuy.TaskbarGroup.Foundation; + +namespace TheXamlGuy.TaskbarGroup +{ + public class TaskbarButtonFlyoutActivationHandler : IMessageHandler + { + private readonly TaskbarButtonFlyoutWindow window; + private readonly TaskbarButtonViewModel taskbarButtonViewModel; + private readonly TaskbarButtonView taskbarButtonView; + private readonly ITaskbar taskbar; + + public TaskbarButtonFlyoutActivationHandler(ITaskbar taskbar, + TaskbarButtonViewModel taskbarButtonViewModel, + TaskbarButtonView taskbarButtonView, + TaskbarButtonFlyoutWindow window) + { + this.taskbar = taskbar; + this.taskbarButtonViewModel = taskbarButtonViewModel; + this.taskbarButtonView = taskbarButtonView; + this.window = window; + } + + public void Handle(TaskbarButtonFlyoutActivation message) + { + var button = message.Button; + var dpiX = window.DpiX(); + var dpiY = window.DpiY(); + + var taskbarState = taskbar.GetCurrentState(); + + var placement = TaskbarButtonFlyoutPlacement.Bottom; + switch (taskbarState.Placement) + { + case TaskbarPlacement.Left: + placement = TaskbarButtonFlyoutPlacement.Left; + break; + + case TaskbarPlacement.Top: + placement = TaskbarButtonFlyoutPlacement.Top; + break; + + case TaskbarPlacement.Right: + placement = TaskbarButtonFlyoutPlacement.Right; + break; + + case TaskbarPlacement.Bottom: + placement = TaskbarButtonFlyoutPlacement.Bottom; + window.Left = ((button.Bounds.X + (button.Bounds.Width / 2)) / dpiX) - (window.Width / 2); + window.Top = (button.Bounds.Y / dpiY) - window.Height; + break; + } + + if (window.XamlContent is TaskbarButtonFlyout flyout) + { + flyout.Margin = new Windows.UI.Xaml.Thickness(6); + + taskbarButtonView.Bind(taskbarButtonViewModel); + flyout.Content = taskbarButtonView; + + flyout.ShowAt(placement); + } + + window.Activate(); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/Properties/Program.cs b/TheXamlGuy.TaskbarGroup/Properties/Program.cs new file mode 100644 index 0000000..2a37422 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/Properties/Program.cs @@ -0,0 +1,17 @@ +using System; + +namespace TheXamlGuy.TaskbarGroup +{ + public class Program + { + [STAThread()] + public static void Main() + { + using (new Flyout.App()) + { + var app = new App(); + app.Run(); + } + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/TheXamlGuy.TaskbarGroup.csproj b/TheXamlGuy.TaskbarGroup/TheXamlGuy.TaskbarGroup.csproj new file mode 100644 index 0000000..96a187b --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/TheXamlGuy.TaskbarGroup.csproj @@ -0,0 +1,26 @@ + + + WinExe + netcoreapp3.1 + true + TheXamlGuy.TaskbarGroup.Program + uap10.0.19041 + x64;x86 + 10.0 + enable + + + + + + + + + + + + + + + + diff --git a/TheXamlGuy.TaskbarGroup/Windows/TaskbarButtonFlyoutWindow.cs b/TheXamlGuy.TaskbarGroup/Windows/TaskbarButtonFlyoutWindow.cs new file mode 100644 index 0000000..f79de11 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/Windows/TaskbarButtonFlyoutWindow.cs @@ -0,0 +1,80 @@ +using System; +using System.Windows; +using System.Windows.Media; +using System.Windows.Threading; +using TheXamlGuy.TaskbarGroup.Core; +using TheXamlGuy.TaskbarGroup.Flyout.Controls; + +namespace TheXamlGuy.TaskbarGroup +{ + public class TaskbarButtonFlyoutWindow : TransparentXamlWindow + { + private readonly IMediator mediator; + private bool isDpiChanging; + + public TaskbarButtonFlyoutWindow(IMessenger messenger, + IMediator mediator) + { + this.mediator = mediator; + + DpiChanged += OnDpiChanged; + Deactivated += OnDeactivated; + Topmost = true; + Width = 258; + Height = 258; + + messenger.Subscribe(OnTaskbarButtonInvoked); + messenger.Subscribe(OnTaskbarButtonDragEnter); + } + + private void OnDeactivated(object? sender, EventArgs args) + { + if (XamlContent is TaskbarButtonFlyout flyout) + { + flyout.Close(); + } + } + + private async void OnDpiChanged(object sender, DpiChangedEventArgs args) + { + if (isDpiChanging) return; + + isDpiChanging = true; + + await Dispatcher.Invoke(async () => + { + Visibility = Visibility.Visible; + await Dispatcher.BeginInvoke(new Action(() => + { + VisualTreeHelper.SetRootDpi(this, args.OldDpi); + }), DispatcherPriority.ContextIdle, null); + + await Dispatcher.BeginInvoke(new Action(() => + { + VisualTreeHelper.SetRootDpi(this, args.NewDpi); + }), DispatcherPriority.ContextIdle, null); + + await Dispatcher.BeginInvoke(new Action(() => + { + Visibility = Visibility.Hidden; + isDpiChanging = false; + }), DispatcherPriority.ContextIdle, null); + }); + } + + private void OnTaskbarButtonDragEnter(TaskbarButtonDragEnter args) + { + Dispatcher.Invoke(() => Open(args.Button)); + } + + private void OnTaskbarButtonInvoked(TaskbarButtonInvoked args) + { + Dispatcher.Invoke(() => Open(args.Button)); + } + + private void Open(TaskbarButton button) + { + mediator.Handle(new TaskbarButtonFlyoutActivation(button)); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/Windows/TransparentXamlWindow.cs b/TheXamlGuy.TaskbarGroup/Windows/TransparentXamlWindow.cs new file mode 100644 index 0000000..7485033 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/Windows/TransparentXamlWindow.cs @@ -0,0 +1,25 @@ +using Microsoft.Toolkit.Wpf.UI.XamlHost; +using System.Windows; +using System.Windows.Media; + +namespace TheXamlGuy.TaskbarGroup +{ + public class TransparentXamlWindow : XamlWindow where TXamlContent : Windows.UI.Xaml.UIElement + { + public TransparentXamlWindow() => PrepareDefaultWindow(); + + protected override WindowsXamlHost OnInitializing(WindowsXamlHost xamlHost) + { + return base.OnInitializing(xamlHost); + } + + private void PrepareDefaultWindow() + { + ShowInTaskbar = false; + WindowStyle = WindowStyle.None; + ResizeMode = ResizeMode.NoResize; + AllowsTransparency = true; + Background = new SolidColorBrush(Colors.Transparent); + } + } +} diff --git a/TheXamlGuy.TaskbarGroup/Windows/XamlWindow.cs b/TheXamlGuy.TaskbarGroup/Windows/XamlWindow.cs new file mode 100644 index 0000000..19ab128 --- /dev/null +++ b/TheXamlGuy.TaskbarGroup/Windows/XamlWindow.cs @@ -0,0 +1,41 @@ +using Microsoft.Toolkit.Wpf.UI.XamlHost; +using System.Windows; + +namespace TheXamlGuy.TaskbarGroup +{ + public class XamlWindow : Window where TXamlContent : Windows.UI.Xaml.UIElement + { + private WindowsXamlHost? xamlHost; + + public XamlWindow() + { + Initialize(); + } + + public TXamlContent? XamlContent + { + get + { + if (xamlHost is null) return null; + return xamlHost.GetUwpInternalObject() as TXamlContent; + } + } + + protected virtual WindowsXamlHost OnInitializing(WindowsXamlHost xamlHost) + { + xamlHost.InitialTypeName = typeof(TXamlContent).FullName; + xamlHost.HorizontalAlignment = HorizontalAlignment.Stretch; + xamlHost.VerticalAlignment = VerticalAlignment.Stretch; + + return xamlHost; + } + + private void Initialize() + { + xamlHost = new WindowsXamlHost(); + OnInitializing(xamlHost); + + Content = xamlHost; + } + } +}