diff --git a/.gitignore b/.gitignore index 2437536c..93fce949 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,459 @@ +## 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/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory .vs/ -*.csproj.user -bin/ -obj/ -Morphic.*Setup/Generated.wxs -Morphic.Client/appsettings.Local.json -Morphic.Client/appsettings.json -Morphic.Client/app.manifest -Morphic.Client/BuildVersion.txt -Morphic.Bar +# 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/ + +# Tye +.tye/ + +# 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 +# NOTE: Packaged apps have publish profiles (.pubxml files) and our WinUI/WinAppSDK dependencies won't be included in our MSIX installer if we don't check them in +#*.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 + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +## +## Repository-specific files +## diff --git a/LICENSE.txt b/LICENSE.txt index 60f5bcf5..8a775bee 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,10 +1,4 @@ -Copyright 2020-2021 Raising the Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt +Copyright 2020-2025 Raising the Floor - US, Inc. The R&D leading to these results received funding from the: * Rehabilitation Services Administration, US Dept. of Education under @@ -19,4 +13,16 @@ The R&D leading to these results received funding from the: * Ontario Ministry of Research and Innovation * Canadian Foundation for Innovation * Adobe Foundation -* Consumer Electronics Association Foundation \ No newline at end of file +* Consumer Electronics Association Foundation + +Licensed under the New BSD License. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Morphic (Package)/Images/LargeTile.scale-100.png b/Morphic (Package)/Images/LargeTile.scale-100.png new file mode 100644 index 00000000..12af4710 Binary files /dev/null and b/Morphic (Package)/Images/LargeTile.scale-100.png differ diff --git a/Morphic (Package)/Images/LargeTile.scale-125.png b/Morphic (Package)/Images/LargeTile.scale-125.png new file mode 100644 index 00000000..fda5644f Binary files /dev/null and b/Morphic (Package)/Images/LargeTile.scale-125.png differ diff --git a/Morphic (Package)/Images/LargeTile.scale-150.png b/Morphic (Package)/Images/LargeTile.scale-150.png new file mode 100644 index 00000000..b27ceb94 Binary files /dev/null and b/Morphic (Package)/Images/LargeTile.scale-150.png differ diff --git a/Morphic (Package)/Images/LargeTile.scale-200.png b/Morphic (Package)/Images/LargeTile.scale-200.png new file mode 100644 index 00000000..27ed2d2f Binary files /dev/null and b/Morphic (Package)/Images/LargeTile.scale-200.png differ diff --git a/Morphic (Package)/Images/LargeTile.scale-400.png b/Morphic (Package)/Images/LargeTile.scale-400.png new file mode 100644 index 00000000..c687c747 Binary files /dev/null and b/Morphic (Package)/Images/LargeTile.scale-400.png differ diff --git a/Morphic (Package)/Images/LockScreenLogo.scale-200.png b/Morphic (Package)/Images/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..79f38efe Binary files /dev/null and b/Morphic (Package)/Images/LockScreenLogo.scale-200.png differ diff --git a/Morphic (Package)/Images/SmallTile.scale-100.png b/Morphic (Package)/Images/SmallTile.scale-100.png new file mode 100644 index 00000000..2c42253b Binary files /dev/null and b/Morphic (Package)/Images/SmallTile.scale-100.png differ diff --git a/Morphic (Package)/Images/SmallTile.scale-125.png b/Morphic (Package)/Images/SmallTile.scale-125.png new file mode 100644 index 00000000..732c4daa Binary files /dev/null and b/Morphic (Package)/Images/SmallTile.scale-125.png differ diff --git a/Morphic (Package)/Images/SmallTile.scale-150.png b/Morphic (Package)/Images/SmallTile.scale-150.png new file mode 100644 index 00000000..2c2a1325 Binary files /dev/null and b/Morphic (Package)/Images/SmallTile.scale-150.png differ diff --git a/Morphic (Package)/Images/SmallTile.scale-200.png b/Morphic (Package)/Images/SmallTile.scale-200.png new file mode 100644 index 00000000..c22722a6 Binary files /dev/null and b/Morphic (Package)/Images/SmallTile.scale-200.png differ diff --git a/Morphic (Package)/Images/SmallTile.scale-400.png b/Morphic (Package)/Images/SmallTile.scale-400.png new file mode 100644 index 00000000..13d10003 Binary files /dev/null and b/Morphic (Package)/Images/SmallTile.scale-400.png differ diff --git a/Morphic (Package)/Images/SplashScreen.scale-100.png b/Morphic (Package)/Images/SplashScreen.scale-100.png new file mode 100644 index 00000000..1d9e88c0 Binary files /dev/null and b/Morphic (Package)/Images/SplashScreen.scale-100.png differ diff --git a/Morphic (Package)/Images/SplashScreen.scale-125.png b/Morphic (Package)/Images/SplashScreen.scale-125.png new file mode 100644 index 00000000..d662b882 Binary files /dev/null and b/Morphic (Package)/Images/SplashScreen.scale-125.png differ diff --git a/Morphic (Package)/Images/SplashScreen.scale-150.png b/Morphic (Package)/Images/SplashScreen.scale-150.png new file mode 100644 index 00000000..c3d899b2 Binary files /dev/null and b/Morphic (Package)/Images/SplashScreen.scale-150.png differ diff --git a/Morphic (Package)/Images/SplashScreen.scale-200.png b/Morphic (Package)/Images/SplashScreen.scale-200.png new file mode 100644 index 00000000..a6b09e57 Binary files /dev/null and b/Morphic (Package)/Images/SplashScreen.scale-200.png differ diff --git a/Morphic (Package)/Images/SplashScreen.scale-400.png b/Morphic (Package)/Images/SplashScreen.scale-400.png new file mode 100644 index 00000000..330fa2fe Binary files /dev/null and b/Morphic (Package)/Images/SplashScreen.scale-400.png differ diff --git a/Morphic (Package)/Images/Square150x150Logo.scale-100.png b/Morphic (Package)/Images/Square150x150Logo.scale-100.png new file mode 100644 index 00000000..6ae7c7ea Binary files /dev/null and b/Morphic (Package)/Images/Square150x150Logo.scale-100.png differ diff --git a/Morphic (Package)/Images/Square150x150Logo.scale-125.png b/Morphic (Package)/Images/Square150x150Logo.scale-125.png new file mode 100644 index 00000000..3d5e9074 Binary files /dev/null and b/Morphic (Package)/Images/Square150x150Logo.scale-125.png differ diff --git a/Morphic (Package)/Images/Square150x150Logo.scale-150.png b/Morphic (Package)/Images/Square150x150Logo.scale-150.png new file mode 100644 index 00000000..62fa8339 Binary files /dev/null and b/Morphic (Package)/Images/Square150x150Logo.scale-150.png differ diff --git a/Morphic (Package)/Images/Square150x150Logo.scale-200.png b/Morphic (Package)/Images/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..6f998621 Binary files /dev/null and b/Morphic (Package)/Images/Square150x150Logo.scale-200.png differ diff --git a/Morphic (Package)/Images/Square150x150Logo.scale-400.png b/Morphic (Package)/Images/Square150x150Logo.scale-400.png new file mode 100644 index 00000000..e6712394 Binary files /dev/null and b/Morphic (Package)/Images/Square150x150Logo.scale-400.png differ diff --git a/Morphic (Package)/Images/StoreLogo.scale-100.png b/Morphic (Package)/Images/StoreLogo.scale-100.png new file mode 100644 index 00000000..d106da1a Binary files /dev/null and b/Morphic (Package)/Images/StoreLogo.scale-100.png differ diff --git a/Morphic (Package)/Images/StoreLogo.scale-125.png b/Morphic (Package)/Images/StoreLogo.scale-125.png new file mode 100644 index 00000000..faa312f0 Binary files /dev/null and b/Morphic (Package)/Images/StoreLogo.scale-125.png differ diff --git a/Morphic (Package)/Images/StoreLogo.scale-150.png b/Morphic (Package)/Images/StoreLogo.scale-150.png new file mode 100644 index 00000000..9c5df07c Binary files /dev/null and b/Morphic (Package)/Images/StoreLogo.scale-150.png differ diff --git a/Morphic (Package)/Images/StoreLogo.scale-200.png b/Morphic (Package)/Images/StoreLogo.scale-200.png new file mode 100644 index 00000000..26bd8fe0 Binary files /dev/null and b/Morphic (Package)/Images/StoreLogo.scale-200.png differ diff --git a/Morphic (Package)/Images/StoreLogo.scale-400.png b/Morphic (Package)/Images/StoreLogo.scale-400.png new file mode 100644 index 00000000..622639c1 Binary files /dev/null and b/Morphic (Package)/Images/StoreLogo.scale-400.png differ diff --git a/Morphic (Package)/Images/Wide310x150Logo.scale-100.png b/Morphic (Package)/Images/Wide310x150Logo.scale-100.png new file mode 100644 index 00000000..2fd23a5b Binary files /dev/null and b/Morphic (Package)/Images/Wide310x150Logo.scale-100.png differ diff --git a/Morphic (Package)/Images/Wide310x150Logo.scale-125.png b/Morphic (Package)/Images/Wide310x150Logo.scale-125.png new file mode 100644 index 00000000..ef2dbfbe Binary files /dev/null and b/Morphic (Package)/Images/Wide310x150Logo.scale-125.png differ diff --git a/Morphic (Package)/Images/Wide310x150Logo.scale-150.png b/Morphic (Package)/Images/Wide310x150Logo.scale-150.png new file mode 100644 index 00000000..145fdba0 Binary files /dev/null and b/Morphic (Package)/Images/Wide310x150Logo.scale-150.png differ diff --git a/Morphic (Package)/Images/Wide310x150Logo.scale-200.png b/Morphic (Package)/Images/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..002c377c Binary files /dev/null and b/Morphic (Package)/Images/Wide310x150Logo.scale-200.png differ diff --git a/Morphic (Package)/Images/Wide310x150Logo.scale-400.png b/Morphic (Package)/Images/Wide310x150Logo.scale-400.png new file mode 100644 index 00000000..b24bc4e7 Binary files /dev/null and b/Morphic (Package)/Images/Wide310x150Logo.scale-400.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-16.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..b00844ea Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-256.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..b41f06a8 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-32.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..82a9f531 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-48.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..d74bf9bc Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-100.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..aa6a0823 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-100.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-125.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..a3cf2164 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-125.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-150.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..4bf212d7 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-150.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-200.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..cb57e40c Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-200.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-400.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..ff6fbf41 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.scale-400.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-128.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-128.png new file mode 100644 index 00000000..942c33a2 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-128.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-16.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..219d811d Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-24.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..32ce302e Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-24.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-256.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..ca6eb74b Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-32.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..468cc148 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-48.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..0f8cc057 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-64.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-64.png new file mode 100644 index 00000000..e78e13c6 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-64.png differ diff --git a/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-96.png b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-96.png new file mode 100644 index 00000000..d067c5f0 Binary files /dev/null and b/Morphic (Package)/Images/contrast-black/Square44x44Logo.targetsize-96.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-16.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..cb3b6b07 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-256.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..f6563c0e Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-32.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..67f25c50 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-48.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..45515853 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-100.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..baabfba4 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-100.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-125.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..1ecb550c Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-125.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-150.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..d1ccf3a5 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-150.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-200.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..1039dfc3 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-200.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-400.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..be0de596 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.scale-400.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-16.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..8c66b9f6 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..b1d09a7d Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24_altform-unplated.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..e0b2adf5 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-256.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..529578b7 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-32.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..f3cf5151 Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-48.png b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..a512470c Binary files /dev/null and b/Morphic (Package)/Images/contrast-standard/Square44x44Logo.targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-16.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..d8db7382 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-256.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..8bfd7a1b Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-32.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..46a0852d Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-48.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..2f809041 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-100.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..eb23f173 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-100.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-125.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..2e191478 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-125.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-150.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..6c287bdf Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-150.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-200.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..e64ad4d5 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-200.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-400.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..7e1b025e Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.scale-400.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-128.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-128.png new file mode 100644 index 00000000..83298a2d Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-128.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-16.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..f4fe2252 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-16.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-24.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..66a1e79d Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-24.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-256.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..98b784ef Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-256.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-32.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..659f2478 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-32.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-48.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..96a558b7 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-48.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-64.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-64.png new file mode 100644 index 00000000..3532b089 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-64.png differ diff --git a/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-96.png b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-96.png new file mode 100644 index 00000000..3e17f453 Binary files /dev/null and b/Morphic (Package)/Images/contrast-white/Square44x44Logo.targetsize-96.png differ diff --git a/Morphic (Package)/Morphic (Package).wapproj b/Morphic (Package)/Morphic (Package).wapproj new file mode 100644 index 00000000..264aae28 --- /dev/null +++ b/Morphic (Package)/Morphic (Package).wapproj @@ -0,0 +1,153 @@ + + + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + + + + 623e4ccf-6c54-4acd-80e7-951e62780a84 + 10.0.22621.0 + 10.0.19041.0 + net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) + en-US + false + $(NoWarn);NU1702 + ..\Morphic\Morphic.csproj + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + Properties\PublishProfiles\win-$(Platform).pubxml + + + + + + + + build + + + + + + + + + \ No newline at end of file diff --git a/Morphic (Package)/Package.appxmanifest b/Morphic (Package)/Package.appxmanifest new file mode 100644 index 00000000..17b08b60 --- /dev/null +++ b/Morphic (Package)/Package.appxmanifest @@ -0,0 +1,50 @@ + + + + + + + + Morphic + Raising the Floor - US Inc + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Morphic.Client/App.xaml b/Morphic.Client/App.xaml deleted file mode 100644 index eca72e28..00000000 --- a/Morphic.Client/App.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - diff --git a/Morphic.Client/App.xaml.cs b/Morphic.Client/App.xaml.cs deleted file mode 100644 index 6646a922..00000000 --- a/Morphic.Client/App.xaml.cs +++ /dev/null @@ -1,1375 +0,0 @@ -// Copyright 2020-2021 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using AutoUpdaterDotNET; -using CountlySDK; -using CountlySDK.Entities; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Morphic.Core; -using Morphic.Service; -using NHotkey.Wpf; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Threading; - -namespace Morphic.Client -{ - using Bar; - using Bar.Data; - using Config; - using CountlySDK.CountlyCommon; - using Dialogs; - using Menu; - using Microsoft.Win32; - using Morphic.Telemetry; - using Morphic.Windows.Native.OsVersion; - using Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - using System.Diagnostics; - using System.Text.Json; - - public class AppMain - { - private static Mutex _singleInstanceMutex; - private static uint _singleInstanceMessageId; - - // NOTE: we created our own Main function so that we can use a mutex to enforce running only one instance of Morphic at a time - [STAThread] - public static void Main() - { - // create a message which we can send/receive to indicate that a secondary instance has been started; use the application ID as its backing unique string - _singleInstanceMessageId = WinApi.RegisterWindowMessage(App.ApplicationId); - - // create a mutex which we will use to make sure only one copy of Morphic runs at a time - bool mutexCreatedNew; - _singleInstanceMutex = new Mutex(true, App.ApplicationId, out mutexCreatedNew); - - // if the mutex already existed (i.e. the application is already running), send a message to it now asking it to show its MorphicBar - if (mutexCreatedNew == false) - { - // send the "single instance" message to the main instance; leave both parameters as zero - MessageWatcherNativeWindow.PostMessage(_singleInstanceMessageId, IntPtr.Zero, IntPtr.Zero); - - // shut down our application (gracefully by returning from Main) - return; - } - - // Ensure the current directory is the same as the executable, so relative paths work. - Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - - App.Main(); - } - - internal static void ReleaseSingleInstanceMutex() - { - _singleInstanceMutex.ReleaseMutex(); - } - - internal static uint SingleInstanceMessageId - { - get - { - return _singleInstanceMessageId; - } - } - } - - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - /// Current application instance. - public new static App Current { get; private set; } = null!; - - public IServiceProvider ServiceProvider { get; private set; } = null!; - public IConfiguration Configuration { get; private set; } = null!; - public ILogger Logger { get; private set; } = null!; - - public MorphicSession MorphicSession { get; private set; } = null!; - - private MorphicTelemetryClient? _telemetryClient = null; - - public AppOptions AppOptions => AppOptions.Current; - - public DialogManager Dialogs { get; } = new DialogManager(); - public BarManager BarManager { get; } = new BarManager(); - - public const string ApplicationId = "A6E8092B-51F4-4CAA-A874-A791152B5698"; - - #region Configuration & Startup - - public App() - { - App.Current = this; - } - - public class MorphicBarExtraItem - { - public string? type { get; set; } - public string? label { get; set; } - public string? tooltipHeader { get; set; } - public string? tooltipText { get; set; } - // for type: link - public string? url { get; set; } - // for type: action - public string? function { get; set; } - // for type: control - public string? feature { get; set; } - } - // - public class TelemetryConfigSection - { - public string? siteId { get; set; } - } - // - public class ConfigFileContents - { - public class FeaturesConfigSection - { - public class EnabledFeature - { - public bool? enabled { get; set; } - public string? scope { get; set; } - } - // - public EnabledFeature? autorunAfterLogin { get; set; } - public EnabledFeature? checkForUpdates { get; set; } - public EnabledFeature? cloudSettingsTransfer { get; set; } - public EnabledFeature? resetSettings { get; set; } - } - public class MorphicBarConfigSection - { - public string? visibilityAfterLogin { get; set; } - public List? extraItems { get; set; } - } - // - public int? version { get; set; } - public FeaturesConfigSection? features { get; set; } - public MorphicBarConfigSection? morphicBar { get; set; } - public TelemetryConfigSection? telemetry { get; set; } - } - // - private struct CommonConfigurationContents - { - public ConfigurableFeatures.AutorunConfigOption? AutorunConfig; - public bool CheckForUpdatesIsEnabled; - public bool CloudSettingsTransferIsEnabled; - public bool ResetSettingsIsEnabled; - public ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption? MorphicBarVisibilityAfterLogin; - public List ExtraMorphicBarItems; - public string? TelemetrySiteId; - } - private async Task GetCommonConfigurationAsync() - { - // set up default configuration - var result = new CommonConfigurationContents(); - // - // autorun - result.AutorunConfig = null; - // - // check for updates - result.CheckForUpdatesIsEnabled = true; - // - // copy settings to/from cloud - result.CloudSettingsTransferIsEnabled = true; - // - // reset settings (to standard) - result.ResetSettingsIsEnabled = false; - // - // morphic bar (visibility and extra items) - result.MorphicBarVisibilityAfterLogin = null; - result.ExtraMorphicBarItems = new List(); - - // NOTE: we have intentionally chosen not to create the CommonConfigDir (e.g. "C:\ProgramData\Morphic") since Morphic does not currently create files in this folder. - var morphicCommonConfigPath = AppPaths.GetCommonConfigDir("", false); - if (Directory.Exists(morphicCommonConfigPath) == false) - { - // no config file; return defaults - return result; - } - - var morphicConfigFilePath = Path.Combine(morphicCommonConfigPath, "config.json"); - if (File.Exists(morphicConfigFilePath) == false) - { - // no config file; return defaults - return result; - } - - string json; - try - { - json = await File.ReadAllTextAsync(morphicConfigFilePath); - } - catch (Exception ex) - { - // error reading config file; return defaults - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Could not read configuration file: " + morphicConfigFilePath + "; error: " + ex.Message); - return result; - } - - ConfigFileContents deserializedJson; - try - { - deserializedJson = JsonSerializer.Deserialize(json); - } - catch (Exception ex) - { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Could not deserialize json configuration file: " + morphicConfigFilePath + "; error: " + ex.Message); - return result; - } - - if ((deserializedJson.version is null) || (deserializedJson.version.Value < 0) || (deserializedJson.version.Value > 0)) - { - // sorry, we don't understand this version of the file - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Unknown config file version: " + deserializedJson.version.ToString()); - return result; - } - - // capture the autorun setting - if (deserializedJson.features?.autorunAfterLogin?.enabled is not null) - { - if (deserializedJson.features!.autorunAfterLogin!.enabled == false) - { - result.AutorunConfig = ConfigurableFeatures.AutorunConfigOption.Disabled; - } - else - { - switch (deserializedJson.features!.autorunAfterLogin!.scope) - { - case "allLocalUsers": - result.AutorunConfig = ConfigurableFeatures.AutorunConfigOption.AllLocalUsers; - break; - case "currentUser": - result.AutorunConfig = ConfigurableFeatures.AutorunConfigOption.CurrentUser; - break; - case null: - // no scope present; use the default scope - break; - default: - // sorry, we don't understand this scope setting - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Unknown autorunAfterLogin scope: " + deserializedJson.features!.autorunAfterLogin!.scope); - return result; - } - } - } - - // capture the check for updates "is enabled" setting - if (deserializedJson.features?.checkForUpdates?.enabled is not null) - { - result.CheckForUpdatesIsEnabled = deserializedJson.features.checkForUpdates.enabled.Value; - } - - // capture the cloud settings transfer "is enabled" setting - if (deserializedJson.features?.cloudSettingsTransfer?.enabled is not null) - { - result.CloudSettingsTransferIsEnabled = deserializedJson.features.cloudSettingsTransfer.enabled.Value; - } - - // capture the reset settings (to standard) "is enabled" setting - if (deserializedJson.features?.resetSettings?.enabled is not null) - { - result.ResetSettingsIsEnabled = deserializedJson.features.resetSettings.enabled.Value; - } - - // capture the desired after-login (autorun) visibility of the MorphicBar - switch (deserializedJson.morphicBar?.visibilityAfterLogin) - { - case "restore": - result.MorphicBarVisibilityAfterLogin = ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Restore; - break; - case "show": - result.MorphicBarVisibilityAfterLogin = ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Show; - break; - case "hide": - result.MorphicBarVisibilityAfterLogin = ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Hide; - break; - case null: - // no setting present; use the default setting - break; - default: - // sorry, we don't understand this visibility setting - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Unknown morphicBar.visibilityAfterLogin setting: " + deserializedJson.morphicBar?.visibilityAfterLogin); - return result; - } - - - // capture any extra items (up to 3) - if (deserializedJson.morphicBar?.extraItems is not null) - { - foreach (var extraItem in deserializedJson.morphicBar!.extraItems) - { - // if we already captured 3 extra items, skip this one - if (result.ExtraMorphicBarItems.Count >= 3) - { - continue; - } - - var extraItemType = extraItem.type; - var extraItemLabel = extraItem.label; - var extraItemTooltipHeader = extraItem.tooltipHeader; - var extraItemTooltipText = extraItem.tooltipText; - // for type: link - var extraItemUrl = extraItem.url; - // for type: action - var extraItemFunction = extraItem.function; - // for type: control - var extraItemFeature = extraItem.feature; - - // if the item is invalid, log the error and skip this item - if (extraItemType is null) - { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Invalid MorphicBar item: " + extraItem.ToString()); - continue; - } - if ((extraItemType != "control") && ((extraItemLabel is null) || (extraItemTooltipHeader is null))) - { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Invalid MorphicBar item: " + extraItem.ToString()); - continue; - } - - // if the "link" is missing its url, log the error and skip this item - if ((extraItemType == "link") && (extraItemUrl is null)) - { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Invalid MorphicBar item: " + extraItem.ToString()); - continue; - } - - // if the "action" is missing its function, log the error and skip this item - if ((extraItemType == "action") && (extraItemFunction is null || extraItemFunction == "")) - { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Invalid MorphicBar item: " + extraItem.ToString()); - continue; - } - - // if the "control" is missing its feature, log the error and skip this item - if ((extraItem.type == "control") && (extraItemFeature is null || extraItemFeature == "")) { - // NOTE: consider refusing to start up (for security reasons) if the configuration file cannot be read - Logger?.LogError("Invalid MorphicBar item: " + extraItem.ToString()); - continue; - } - - var extraMorphicBarItem = new MorphicBarExtraItem(); - extraMorphicBarItem.type = extraItemType; - extraMorphicBarItem.label = extraItemLabel; - extraMorphicBarItem.tooltipHeader = extraItemTooltipHeader; - extraMorphicBarItem.tooltipText = extraItemTooltipText; - extraMorphicBarItem.url = extraItemUrl; - extraMorphicBarItem.function = extraItemFunction; - extraMorphicBarItem.feature = extraItemFeature; - result.ExtraMorphicBarItems.Add(extraMorphicBarItem); - } - } - - // capture telemetry site id - result.TelemetrySiteId = deserializedJson.telemetry?.siteId; - - return result; - } - - private bool ShouldTelemetryBeDisabled() - { - // NOTE: we have intentionally chosen not to create the CommonConfigDir (e.g. "C:\ProgramData\Morphic") since Morphic does not currently create files in this folder. - var morphicCommonConfigPath = AppPaths.GetCommonConfigDir("", false); - if (Directory.Exists(morphicCommonConfigPath) == false) - { - // if the Morphic common config path doesn't exist, there's definitely no file - return false; - } - // - var disableTelemetryFilePath = Path.Combine(morphicCommonConfigPath, "disable_telemetry.txt"); - - // if disable_telemetry.txt exists, disable telemetry - var disableTelemetryFileExists = File.Exists(disableTelemetryFilePath); - return disableTelemetryFileExists; - } - - /// - /// Create a Configuration from appsettings.json - /// - /// - private IConfiguration GetConfiguration() - { - ConfigurationBuilder builder = new ConfigurationBuilder(); - builder.SetBasePath(Directory.GetCurrentDirectory()); - builder.AddJsonFile("appsettings.json", optional: false); - if (this.AppOptions.Launch.Debug) - { - builder.AddJsonFile("appsettings.Debug.json", optional: true); - builder.AddJsonFile("appsettings.Local.json", optional: true); - } - builder.AddEnvironmentVariables(); - return builder.Build(); - } - - /// - /// Configure the dependency injection system with services - /// - /// - private void ConfigureServices(IServiceCollection services) - { - services.AddLogging(this.ConfigureLogging); - services.Configure(this.Configuration.GetSection("MorphicService")); - services.Configure(this.Configuration.GetSection("Update")); - services.AddSingleton(services); - services.AddSingleton(provider => provider); - services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); - services.AddSingleton(new StorageOptions { RootPath = AppPaths.GetUserLocalConfigDir("Data") }); - services.AddSingleton(new KeychainOptions { Path = AppPaths.GetUserLocalConfigDir("keychain") }); - services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService>().Value); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(s => BarPresets.Default); - services.AddSolutionsRegistryServices(); - services.AddSingleton(s => Solutions.FromFile(s, AppPaths.GetAppFile("solutions.json5"))); - } - - internal async Task Countly_RecordEventAsync(string Key) { - if (ConfigurableFeatures.TelemetryIsEnabled == true) - { - await Countly.RecordEvent(Key); - - _telemetryClient?.EnqueueActionMessage(Key); - } - } - - internal async Task Countly_RecordEventAsync(string Key, int Count, Segmentation Segmentation) - { - if (ConfigurableFeatures.TelemetryIsEnabled == true) - { - await Countly.RecordEvent(Key, Count, Segmentation); - - _telemetryClient?.EnqueueActionMessage(Key); - } - } - - private struct TelemetryIdComponents - { - public string CompositeId; - public string? SiteId; - public string DeviceUuid; - } - private TelemetryIdComponents GetOrCreateTelemetryIdComponents() - { - // retrieve the telemetry device ID for this device; if it doesn't exist then create a new one - var telemetryCompositeId = AppOptions.TelemetryDeviceUuid; - if ((telemetryCompositeId is null) || (telemetryCompositeId == String.Empty) || (telemetryCompositeId.IndexOf("D_") < 0)) - { - telemetryCompositeId = "D_" + Guid.NewGuid().ToString(); - AppOptions.TelemetryDeviceUuid = telemetryCompositeId; - } - - // if a site id is (or is not) configured, modify the telemetry device uuid accordingly - // NOTE: we handle cases of site ids changing, site IDs being added post-deployment, and site IDs being removed post-deployment - var unmodifiedTelemetryDeviceCompositeId = telemetryCompositeId; - var telemetrySiteId = ConfigurableFeatures.TelemetrySiteId; - if ((telemetrySiteId is not null) && (telemetrySiteId != String.Empty)) - { - // NOTE: in the future, consider reporting or throwing an error if the site id required sanitization (i.e. wasn't valid) - var sanitizedTelemetrySiteId = this.SanitizeSiteId(telemetrySiteId); - if (sanitizedTelemetrySiteId != "") - { - // we have a telemetry site id; prepend it - telemetryCompositeId = this.PrependSiteIdToTelemetryCompositeId(telemetryCompositeId, sanitizedTelemetrySiteId); - } - else - { - // the supplied site id isn't valid; strip off the site id; In the future consider logging/reporting an error - telemetryCompositeId = this.RemoveSiteIdFromTelemetryCompositeId(telemetryCompositeId); - } - } - else - { - // no telemetry site id is configured; strip off any site id which might have already been part of our telemetry id - // TODO: in the future, make sure that the telemetry ID wasn't _just_ the site id (as it cannot be allowed to be empty) - telemetryCompositeId = this.RemoveSiteIdFromTelemetryCompositeId(telemetryCompositeId); - } - // if the telemetry uuid has changed (because of the site id), update our stored telemetry uuid now - if (telemetryCompositeId != unmodifiedTelemetryDeviceCompositeId) - { - AppOptions.TelemetryDeviceUuid = telemetryCompositeId; - } - - // capture the raw device UUID - var indexOfTelemetryDeviceUuid = telemetryCompositeId.IndexOf("D_") + 2; - var telemetryDeviceUuid = telemetryCompositeId.Substring(indexOfTelemetryDeviceUuid); - - return new TelemetryIdComponents() { CompositeId = telemetryCompositeId, SiteId = telemetrySiteId, DeviceUuid = telemetryDeviceUuid }; - } - - private async Task ConfigureCountlyAsync() - { - // TODO: Move metrics related things to own class. - - // retrieve the telemetry composite ID for this device; if it doesn't exist then create a new one - var telemetryDeviceCompositeId = this.GetOrCreateTelemetryIdComponents().CompositeId; - - IConfigurationSection? section = this.Configuration.GetSection("Countly"); - CountlyConfig cc = new CountlyConfig - { - serverUrl = section["ServerUrl"], - appKey = section["AppKey"], - appVersion = BuildInfo.Current.InformationalVersion, - developerProvidedDeviceId = telemetryDeviceCompositeId - }; - - await Countly.Instance.Init(cc); - await Countly.Instance.SessionBegin(); - CountlyBase.IsLoggingEnabled = true; - } - - private string PrependSiteIdToTelemetryCompositeId(string value, string telemetrySiteId) - { - var telemetryDeviceUuid = value; - - if (telemetryDeviceUuid.StartsWith("S_")) - { - // if the telemetry device uuid already starts with a site id, strip it off now - telemetryDeviceUuid = telemetryDeviceUuid.Remove(0, 2); - var indexOfForwardSlash = telemetryDeviceUuid.IndexOf('/'); - if (indexOfForwardSlash >= 0) - { - // strip the site id off the front - telemetryDeviceUuid = telemetryDeviceUuid.Substring(indexOfForwardSlash + 1); - } - else - { - // the site ID was the only contents; return null - telemetryDeviceUuid = ""; - } - } - - // prepend the site id to the telemetry device uuid - telemetryDeviceUuid = "S_" + telemetrySiteId + "/" + telemetryDeviceUuid; - return telemetryDeviceUuid; - } - - private string RemoveSiteIdFromTelemetryCompositeId(string value) - { - var telemetryDeviceUuid = value; - - if (telemetryDeviceUuid.StartsWith("S_")) - { - // if the telemetry device uuid starts with a site id, strip it off now - telemetryDeviceUuid = telemetryDeviceUuid.Remove(0, 2); - var indexOfForwardSlash = telemetryDeviceUuid.IndexOf('/'); - if (indexOfForwardSlash >= 0) - { - // strip the site id off the front - telemetryDeviceUuid = telemetryDeviceUuid.Substring(indexOfForwardSlash + 1); - } - else - { - // the site ID is the only contents - telemetryDeviceUuid = ""; - } - } - - return telemetryDeviceUuid; - } - - private string SanitizeSiteId(string siteId) - { - var siteIdAsCharacters = siteId.ToCharArray(); - var resultAsCharacters = new List(); - foreach (var character in siteIdAsCharacters) - { - if ((character >= 'a' && character <= 'z') || - (character >= 'A' && character <= 'Z') || - (character >= '0' && character <= '9')) - { - resultAsCharacters.Add(character); - } - else - { - // filter out this character - } - - } - - return new string(resultAsCharacters.ToArray()); - } - - #region Telemetry - - private void ConfigureTelemetry() - { - // TODO: Move metrics related things to own class. - - // retrieve the telemetry device ID for this device; if it doesn't exist then create a new one - var telemetryIds = this.GetOrCreateTelemetryIdComponents(); - var telemetryCompositeId = telemetryIds.CompositeId; - var telemetrySiteId = telemetryIds.SiteId; - var telemetryDeviceUuid = telemetryIds.DeviceUuid; - - // configure our telemetry uplink - IConfigurationSection? section = this.Configuration.GetSection("Telemetry"); - var mqttHostname = section["ServerHostname"]; - var mqttClientId = telemetryDeviceUuid; - var mqttUsername = section["AppName"]; - var mqttAnonymousPassword = section["AppKey"]; - - var mqttConfig = new MorphicTelemetryClient.WebsocketTelemetryClientConfig() - { - Hostname = mqttHostname, - Port = 443, - Path = "/ws", - ClientId = mqttClientId, - Username = mqttUsername, - Password = mqttAnonymousPassword, - UseTls = true - }; - var telemetryClient = new MorphicTelemetryClient(mqttConfig); - telemetryClient.SiteId = telemetrySiteId; - _telemetryClient = telemetryClient; - - Task.Run(async () => - { - await telemetryClient.StartSessionAsync(); - }); - } - - #endregion Telemetry - - private void RecordedException(Task task) - { - if (task.Exception is Exception e) - { - this.Logger.LogError("exception thrown while countly recording exception: {msg}", e.Message); - throw e; - } - this.Logger.LogDebug("successfully recorded countly exception"); - } - - void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) - { - // TODO: Improve error logging/reporting. - - Exception ex = e.Exception; - - try - { - this.Logger.LogError("handled uncaught exception: {msg}", ex.Message); - this.Logger.LogError(ex.StackTrace); - - Dictionary extraData = new Dictionary(); - CountlyBase.RecordException(ex.Message, ex.StackTrace, extraData, true) - .ContinueWith(this.RecordedException, TaskScheduler.FromCurrentSynchronizationContext()); - } - catch (Exception) - { - // ignore - } - - Console.WriteLine(ex); - - // in case of unhandled exception, attempt a graceful shutdown - // - // uncomment the following line (if it's useful) for (and only during) debugging -// MessageBox.Show($"Morphic ran into a problem:\n\n{e.Exception.Message}\n\nFurther information:\n{e.Exception}", "Morphic", MessageBoxButton.OK, MessageBoxImage.Warning); - // - try - { - this.BarManager.CloseBar(); - } - catch { } - // - try - { - this.Shutdown(); - } - catch - { - // if we were unable to shutdown the application, hard-exit instead - System.Environment.Exit(1); - } - - // This prevents the exception from crashing the application - e.Handled = true; - } - - /// - /// Configure the logging for the application - /// - /// - private void ConfigureLogging(ILoggingBuilder logging) - { - logging.AddConfiguration(this.Configuration); - logging.AddConsole(); - logging.AddFile(this.AppOptions.Launch.Logfile, options => - { - options.Append = true; - options.FileSizeLimitBytes = 0x100000; - options.MaxRollingFiles = 3; - }); - logging.SetMinimumLevel(LogLevel.Debug); - logging.AddDebug(); - } - - private static List CompatibleWindowsVersions = new List() - { - // NOTE: the first entry in this list represents the "minimum" version of Windows which we support - WindowsVersion.Win10_v1809, - WindowsVersion.Win10_v1903, - WindowsVersion.Win10_v1909, - WindowsVersion.Win10_v2004, - WindowsVersion.Win10_v20H2, - WindowsVersion.Win10_v21H1, - WindowsVersion.Win10_v21H2, - WindowsVersion.Win10_vFuture, - // - WindowsVersion.Win11_v21H2, - WindowsVersion.Win11_vFuture - }; - private static bool IsOsCompatibleWithMorphic() - { - var windowsVersion = OsVersion.GetWindowsVersion(); - - if (windowsVersion is null) - { - // not a valid version - return false; - } - else if (App.CompatibleWindowsVersions.Contains(windowsVersion.Value) == true) - { - return true; - } - else - { - // either this is an old verison of Windows or it's one we missed that we do support - Debug.Assert(false, "Incompatible or unknown version of Windows"); - return false; - } - } - - protected override async void OnStartup(StartupEventArgs e) - { - this.Dispatcher.UnhandledException += this.App_DispatcherUnhandledException; - - if (App.IsOsCompatibleWithMorphic() == false) - { - MessageBox.Show($"Morphic is not compatible with the current version of Windows.\r\n\r\nPlease upgrade to Windows 10 " + App.CompatibleWindowsVersions[0] + " or newer."); - - this.Shutdown(); - return; - } - - this.Configuration = this.GetConfiguration(); - ServiceCollection collection = new ServiceCollection(); - this.ConfigureServices(collection); - this.ServiceProvider = collection.BuildServiceProvider(); - - base.OnStartup(e); - this.Logger = this.ServiceProvider.GetRequiredService>(); - - // determine if telemetry should be enabled - var telemetryShouldBeDisabled = this.ShouldTelemetryBeDisabled(); - var telemetryIsEnabled = (telemetryShouldBeDisabled == false); - - // load (optional) common configuration file - // NOTE: we currently load this AFTER setting up the logger because the GetCommonConfigurationAsync function logs config file errors to the logger - var commonConfiguration = await this.GetCommonConfigurationAsync(); - ConfigurableFeatures.SetFeatures( - autorunConfig: commonConfiguration.AutorunConfig, - checkForUpdatesIsEnabled: commonConfiguration.CheckForUpdatesIsEnabled, - cloudSettingsTransferIsEnabled: commonConfiguration.CloudSettingsTransferIsEnabled, - resetSettingsIsEnabled: commonConfiguration.ResetSettingsIsEnabled, - telemetryIsEnabled: telemetryIsEnabled, - morphicBarvisibilityAfterLogin: commonConfiguration.MorphicBarVisibilityAfterLogin, - morphicBarExtraItems: commonConfiguration.ExtraMorphicBarItems, - telemetrySiteId: commonConfiguration.TelemetrySiteId - ); - - this.MorphicSession = this.ServiceProvider.GetRequiredService(); - this.MorphicSession.UserChangedAsync += this.Session_UserChangedAsync; - - this.Logger.LogInformation("App Started"); - - this.morphicMenu = new MorphicMenu(); - - this.RegisterGlobalHotKeys(); - - if (ConfigurableFeatures.TelemetryIsEnabled == true) - { - await this.ConfigureCountlyAsync(); - this.ConfigureTelemetry(); - } - - if (ConfigurableFeatures.CheckForUpdatesIsEnabled == true) - { - this.StartCheckingForUpdates(); - } - - this.AddSettingsListener(); - - this.BarManager.BarLoaded += BarManager_BarLoaded; - - await this.OpenSessionAsync(); - - // Make settings displayed on the UI update when a system setting has changed, or when the app is focused. - this.SystemSettingChanged += (sender, args) => SettingsHandler.SystemSettingChanged(); - AppFocus.Current.MouseEnter += (sender, args) => SettingsHandler.SystemSettingChanged(); - AppFocus.Current.Activated += (sender, args) => SettingsHandler.SystemSettingChanged(); - } - - /// - /// Actions to perform when this instance is the first since installation. - /// - private async Task OnFirstRun() - { - this.Logger.LogInformation("Performing first-run tasks"); - - // Set the magnifier to lens mode at 200% - Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\ScreenMagnifier", "Magnification", 200); - Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\ScreenMagnifier", "MagnificationMode", 3); - - // Set the colour filter type - if it's not currently enabled. - //bool filterOn = this.MorphicSession.GetBool(SettingsManager.Keys.WindowsDisplayColorFilterEnabled) == true; - bool filterOn = - await this.MorphicSession.GetSetting(SettingId.ColorFiltersEnabled); - if (!filterOn) - { - await this.MorphicSession.SetSetting(SettingId.ColorFiltersFilterType, 5); - } - - // Set the high-contrast theme, if high-contrast is off. - bool highcontrastOn = await this.MorphicSession.GetSetting(SettingId.HighContrastEnabled); - if (!highcontrastOn) - { - // change the user's high contrast theme to the yellow-on-black high contrast theme (theme #1) - Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes", - "LastHighContrastTheme", @"%SystemRoot\resources\Ease of Access Themes\hc1.theme", - RegistryValueKind.ExpandString); - // - // For windows 10 1809+ - Registry.SetValue(@"HKEY_CURRENT_USER\Control Panel\Accessibility\HighContrast", - "High Contrast Scheme", "High Contrast #1"); - } - } - - private async Task ResetSettingsAsync() - { - // NOTE: we want to move these defaults to config.json, and we want to modify the solutions registry to allow _all_ settings to be specified, - // with defaults, in config.json. - - // default values - var colorFiltersEnabledDefault = false; - var darkModeEnabledDefault = false; - var highContrastEnabledDefault = false; - // - // NOTE: displayDpiOffsetDefault realistically needs to be a fixed value ("recommended value") until we have logic to adjust by a relative % - int displayDpiOffsetDefault = 0; - // - var nightModeIsEnabled = false; - - // verify that settings are reset to their default values; if they are not, then set them now - // NOTE: we do these in an order that makes sense during logout (i.e. we try to do as much as we can before Windows wants to close us, so we push - // settings like screen scaling, dark mode and high contrast to the end since they take much longer to change) - // - // color filters - if (await this.MorphicSession.GetSetting(SettingId.ColorFiltersEnabled) != colorFiltersEnabledDefault) - { - await this.MorphicSession.SetSetting(SettingId.ColorFiltersEnabled, colorFiltersEnabledDefault); - } - // - // night mode - if (await this.MorphicSession.GetSetting(SettingId.NightModeEnabled) != nightModeIsEnabled) - { - await this.MorphicSession.SetSetting(SettingId.NightModeEnabled, nightModeIsEnabled); - } - // - // screen scaling - var monitorName = Morphic.Windows.Native.Display.Display.GetMonitorName(null); - if (monitorName is not null) - { - // get the adapterId and sourceId for this monitor - var adapterIdAndSourceId = Morphic.Windows.Native.Display.Display.GetAdapterIdAndSourceId(monitorName); - if (adapterIdAndSourceId is not null) - { - // get the current DPI offset - var currentDisplayDpiOffset = Morphic.Windows.Native.Display.Display.GetCurrentDpiOffsetAndRange(adapterIdAndSourceId.Value.adapterId, adapterIdAndSourceId.Value.sourceId); - if (currentDisplayDpiOffset is not null) - { - if (currentDisplayDpiOffset.Value.currentDpiOffset != displayDpiOffsetDefault) - { - _ = Morphic.Windows.Native.Display.Display.SetDpiOffset(displayDpiOffsetDefault, adapterIdAndSourceId.Value); - } - } - } - } - // - // - // high contrast - if (await this.MorphicSession.GetSetting(SettingId.HighContrastEnabled) != highContrastEnabledDefault) - { - await this.MorphicSession.SetSetting(SettingId.HighContrastEnabled, highContrastEnabledDefault); - } - // - // dark mode - // NOTE: due to the interrelation between high contrast and dark mode, we reset dark mode AFTER resetting high contrast mode - var darkModeIsEnabledResult = await Morphic.Client.Bar.Data.Actions.Functions.GetDarkModeStateAsync(); - if (darkModeIsEnabledResult.IsSuccess == true) - { - var darkModeIsEnabled = darkModeIsEnabledResult.Value!; - - if (darkModeIsEnabled != darkModeEnabledDefault) - { - await Morphic.Client.Bar.Data.Actions.Functions.SetDarkModeStateAsync(darkModeEnabledDefault); - } - } - // - // - // word simplify - var isOfficeInstalled = Morphic.Integrations.Office.WordRibbon.IsOfficeInstalled(); - if (isOfficeInstalled == true) - { - var isBasicSimplifyRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsBasicSimplifyRibbonEnabled(); - if (isBasicSimplifyRibbonEnabledResult.IsSuccess == true) - { - var isBasicSimplifyRibbonEnabled = isBasicSimplifyRibbonEnabledResult.Value!; - if (isBasicSimplifyRibbonEnabled == true) - { - _ = Morphic.Integrations.Office.WordRibbon.DisableBasicSimplifyRibbon(); - } - } - - var isEssentialsSimplifyRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsEssentialsSimplifyRibbonEnabled(); - if (isEssentialsSimplifyRibbonEnabledResult.IsSuccess == true) - { - var isEssentialsSimplifyRibbonEnabled = isEssentialsSimplifyRibbonEnabledResult.Value!; - if (isEssentialsSimplifyRibbonEnabled == true) - { - _ = Morphic.Integrations.Office.WordRibbon.DisableEssentialsSimplifyRibbon(); - } - } - } - } - - private async Task Session_UserChangedAsync(object? sender, MorphicSession.MorphicSessionSignInOrOutEventArgs e) - { - if (sender is MorphicSession morphicSession) - { - if (morphicSession.SignedIn) - { - var lastCommunityId = AppOptions.Current.LastCommunity; - var lastMorphicbarId = AppOptions.Current.LastMorphicbarId; - if (lastCommunityId is not null) - { - // if the user previously selected a community bar, show that one now - // NOTE: the behavior here may be inconsistent with Morphic on macOS. If the previously-selected bar is no longer valid (e.g. the user was removed from the community), - // then we should select the first bar in their list (or the next one...depending on what the design spec says); we should do this consistently on both Windows and macOS - await this.BarManager.LoadSessionBarAsync(morphicSession, lastCommunityId, lastMorphicbarId); - } - else - { - string? newUserSelectedCommunityId = null; - if (e.SignedInViaLoginForm == true) - { - // if the user just signed in and they have not previously selected a community bar on this computer, select their first-available bar in the list (and fall-back to the Basic bar) - if (morphicSession.Communities.Length > 0) { - newUserSelectedCommunityId = morphicSession.Communities[0].Id; - } - } - - if (newUserSelectedCommunityId is not null) - { - await this.BarManager.LoadSessionBarAsync(morphicSession, newUserSelectedCommunityId, null); - } - else - { - // if the user has not selected a community bar, show the basic bar - this.BarManager.LoadBasicMorphicBar(); - } - } - } - else - { - // if no user is signed in, clear out the last community tag - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - - // if no user is signed in, load the basic bar - this.BarManager.LoadBasicMorphicBar(); - } - - // reload our list of communities and re-select the current bar - ResyncCustomMorphicBarMenuItems(); - } - } - - private void BarManager_BarLoaded(object? sender, BarEventArgs e) - { - ResyncCustomMorphicBarMenuItems(); - } - - private void ResyncCustomMorphicBarMenuItems() - { - // clear all communities in the menu (before basic) - var changeMorphicBarMenuItems = this.morphicMenu.ChangeMorphicBar.Items; - var numberOfMenuItems = changeMorphicBarMenuItems.Count; - for (int i = 0; i < numberOfMenuItems; i++) - { - var submenuItem = (MenuItem)changeMorphicBarMenuItems[0]; - if (submenuItem.Name == "SelectBasicMorphicBar") - { - // when we reach the basic MorphicBar entry, exit our loop (so that we don't clear out any remaining items) - break; - } - else - { - this.morphicMenu.ChangeMorphicBar.Items.RemoveAt(0); - } - } - - bool addedCheckmarkByCurrentCommunityBar = false; - - for (int iCommunity = 0; iCommunity < this.MorphicSession.Communities.Length; iCommunity++) - { - var community = this.MorphicSession.Communities[iCommunity]; - // - var allBarsForCommunity = this.MorphicSession.MorphicBarsByCommunityId[community.Id]; - if (allBarsForCommunity is null) - { - // NOTE: this scenario shouldn't happen, but it's a gracefully-degrading failsafe just in case - continue; - } - foreach (var communityBar in allBarsForCommunity) - { - var newMenuItem = new MenuItem(); - newMenuItem.Header = communityBar.Name + " (from " + community.Name + ")"; - newMenuItem.Tag = community.Id + "/" + communityBar.Id; - // - if (community.Id == AppOptions.Current.LastCommunity) - { - var markThisBar = false; - if (AppOptions.Current.LastMorphicbarId is null && addedCheckmarkByCurrentCommunityBar == false) - { - markThisBar = true; - } - else if (AppOptions.Current.LastMorphicbarId == communityBar.Id) - { - markThisBar = true; - } - if (markThisBar == true) - { - newMenuItem.IsChecked = true; - addedCheckmarkByCurrentCommunityBar = true; - } - } - newMenuItem.Click += CustomMorphicBarMenuItem_Click; - // - this.morphicMenu.ChangeMorphicBar.Items.Insert(Math.Max(this.morphicMenu.ChangeMorphicBar.Items.Count - 1, 0), newMenuItem); - } - } - - // if no custom bar was checked, mark the basic bar instead - this.morphicMenu.SelectBasicMorphicBar.IsChecked = (addedCheckmarkByCurrentCommunityBar == false); - } - - private async void CustomMorphicBarMenuItem_Click(object sender, RoutedEventArgs e) - { - var senderAsMenuItem = (MenuItem)sender; - //var communityName = senderAsMenuItem.Header; - var (communityId, morphicbarId) = this.ParseMorphicbarMenuItemTag(senderAsMenuItem); - - await this.BarManager.LoadSessionBarAsync(this.MorphicSession, communityId, morphicbarId); - } - - private (string communityId, string? morphicbarId) ParseMorphicbarMenuItemTag(MenuItem menuItem) - { - string communityId; - string? morphicbarId; - - string tag = (string)menuItem.Tag; - if (tag.IndexOf("/") >= 0) - { - communityId = tag.Substring(0, tag.IndexOf('/')); - morphicbarId = tag.Substring(tag.IndexOf('/') + 1); - } - else - { - communityId = tag; - morphicbarId = null; - } - - return (communityId, morphicbarId); - } - - private void RegisterGlobalHotKeys() - { - EventHandler loginHotKeyPressed = async (sender, e) => - { - // NOTE: if we want the login menu item to apply cloud-saved preferences after login, we should set this flag to true - var applyPreferencesAfterLogin = ConfigurableFeatures.CloudSettingsTransferIsEnabled; - var args = new Dictionary() { { "applyPreferencesAfterLogin", applyPreferencesAfterLogin } }; - await this.Dialogs.OpenDialogAsync(args); - }; - try - { - HotkeyManager.Current.AddOrReplace("Login with Morphic", Key.M, ModifierKeys.Control | ModifierKeys.Shift, loginHotKeyPressed); - } - catch - { - this.Logger.LogError("Could not register hotkey Ctrl+Shift+M for 'Login with Morphic'"); - } - - EventHandler showMorphicBarHotKeyPressed = (sender, e) => - { - this.BarManager.ShowBar(); - }; - try - { - // TODO: should this hotkey be titled "Show MorphicBar" instead? - //HotkeyManager.Current.AddOrReplace("Show Morphic", Key.M, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt, showMorphicBarHotKeyPressed); - // - // NOTE: per request on 10-May-2021, this hotkey has been changed from Ctrl+Shift+Alt+M to Ctrl+Shift+Alt+Windows+M - // TODO: consider changing this modifier key sequence back to Ctrl+Shift+Alt+M (and providing a dialog for the user to decide what key combo they wish to use) - HotkeyManager.Current.AddOrReplace("Show Morphic", Key.M, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt | ModifierKeys.Windows, showMorphicBarHotKeyPressed); - } - catch - { - this.Logger.LogError("Could not register hotkey Ctrl+Shift+Alt+M for 'Show Morphic'"); - } - } - - public async Task OpenSessionAsync() - { - await this.MorphicSession.OpenAsync(); - - // TODO: when the user first runs Morphic, we probably want to open a welcome window (where the user could then log in) - //await this.Dialogs.OpenDialog(); - - this.OnSessionOpened(); - } - - /// - /// Called when the session open task completes - /// - /// - private async void OnSessionOpened() - { - this.Logger.LogInformation("Session Open"); - - if (ConfigurableFeatures.ResetSettingsIsEnabled == true) - { - await this.ResetSettingsAsync(); - } - - if (this.AppOptions.FirstRun) - { - await this.OnFirstRun(); - } - - // if no bar was already loaded, load the Basic bar - if (this.BarManager.BarIsLoaded == false) { - this.BarManager.LoadBasicMorphicBar(); - } - } - - #endregion - - /// - /// The main menu shown from the system tray icon - /// - private MorphicMenu? morphicMenu; - - internal async Task ShowMenuAsync(Control? control = null, MorphicMenu.MenuOpenedSource? menuOpenedSource = null) - { - await this.morphicMenu?.ShowAsync(control, menuOpenedSource); - } - - #region Updates - - void StartCheckingForUpdates() - { - UpdateOptions? options = this.ServiceProvider.GetRequiredService(); - if (options.AppCastUrl != "") - { - AutoUpdater.Start(options.AppCastUrl); - } - } - - #endregion - - private MessageWatcherNativeWindow? _messageWatcherNativeWindow; - - protected override void OnActivated(EventArgs e) - { - if (_messageWatcherNativeWindow is null) - { - // create a list of the messages we want to watch for - List messagesToWatch = new List(); - messagesToWatch.Add(AppMain.SingleInstanceMessageId); // this is the message that lets us know that another instance of Morphic was started up - - _messageWatcherNativeWindow = new MessageWatcherNativeWindow(messagesToWatch); - _messageWatcherNativeWindow.WatchedMessageEvent += _messageWatcherNativeWindow_WatchedMessageEvent; - try - { - _messageWatcherNativeWindow.Initialize(); - } - catch (Exception ex) - { - this.Logger.LogError("could not create messages watcher window: {msg}", ex.Message); - } - } - - base.OnActivated(e); - } - - private void _messageWatcherNativeWindow_WatchedMessageEvent(object sender, MessageWatcherNativeWindow.WatchedMessageEventArgs args) - { - this.BarManager.ShowBar(); - } - - #region Shutdown - - protected override async void OnExit(ExitEventArgs e) - { - _messageWatcherNativeWindow?.Dispose(); - if (ConfigurableFeatures.TelemetryIsEnabled == true) - { - try - { - await Countly.Instance.SessionEnd(); - } - catch { } - // - try - { - if (_telemetryClient is not null) - { - _telemetryClient.StopSessionAsync(); - } - } - catch { } - } - - if (ConfigurableFeatures.ResetSettingsIsEnabled == true) - { - await this.ResetSettingsAsync(); - } - - AppMain.ReleaseSingleInstanceMutex(); - - base.OnExit(e); - } - - #endregion - - #region SystemEvents - - public event EventHandler? SystemSettingChanged; - - private bool addedSystemEvents; - private DispatcherTimer? systemSettingTimer; - - /// - /// Start listening to some changes to system settings. - /// - private void AddSettingsListener() - { - if (this.addedSystemEvents) - { - return; - } - - this.addedSystemEvents = true; - this.systemSettingTimer = new DispatcherTimer(DispatcherPriority.Render) - { - Interval = TimeSpan.FromMilliseconds(500) - }; - - this.systemSettingTimer.Tick += (sender, args) => - { - this.systemSettingTimer.Stop(); - this.SystemSettingChanged?.Invoke(this, EventArgs.Empty); - }; - - SystemEvents.DisplaySettingsChanged += this.SystemEventsOnDisplaySettingsChanged; - SystemEvents.UserPreferenceChanged += this.SystemEventsOnDisplaySettingsChanged; - - SystemEvents.SessionEnding += SystemEvents_SessionEnding; - - this.Exit += (sender, args) => - { - SystemEvents.DisplaySettingsChanged -= this.SystemEventsOnDisplaySettingsChanged; - SystemEvents.UserPreferenceChanged -= this.SystemEventsOnDisplaySettingsChanged; - }; - } - - private void SystemEventsOnDisplaySettingsChanged(object? sender, EventArgs e) - { - // Wait a bit, to see if any other events have been raised. - this.systemSettingTimer?.Start(); - } - - private async void SystemEvents_SessionEnding(object sender, SessionEndingEventArgs e) - { - // NOTE: in our preliminary testing, we do not have enough time during shutdown - // to call/complete this function; we should look for a way to keep Windows from - // forcibly logging out until we have completed our settings reset (or at least a few - // critical 'reset settings' items) - if (ConfigurableFeatures.ResetSettingsIsEnabled == true) - { - await this.ResetSettingsAsync(); - } - } - - #endregion - } -} diff --git a/Morphic.Client/AssemblyInfo.cs b/Morphic.Client/AssemblyInfo.cs deleted file mode 100644 index 22112342..00000000 --- a/Morphic.Client/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly:ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/Morphic.Client/Assets/bar-icons/calendar.xaml b/Morphic.Client/Assets/bar-icons/calendar.xaml deleted file mode 100644 index 5e49ca7a..00000000 --- a/Morphic.Client/Assets/bar-icons/calendar.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/comments.xaml b/Morphic.Client/Assets/bar-icons/comments.xaml deleted file mode 100644 index fb833d57..00000000 --- a/Morphic.Client/Assets/bar-icons/comments.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/envelope-open-text.xaml b/Morphic.Client/Assets/bar-icons/envelope-open-text.xaml deleted file mode 100644 index 0205cd79..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-open-text.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/envelope-open.xaml b/Morphic.Client/Assets/bar-icons/envelope-open.xaml deleted file mode 100644 index 906f9967..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-open.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/envelope-outline-open.xaml b/Morphic.Client/Assets/bar-icons/envelope-outline-open.xaml deleted file mode 100644 index afc45deb..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-outline-open.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/envelope-outline.xaml b/Morphic.Client/Assets/bar-icons/envelope-outline.xaml deleted file mode 100644 index 68c05832..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-outline.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/envelope.xaml b/Morphic.Client/Assets/bar-icons/envelope.xaml deleted file mode 100644 index 7a4e12e4..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/favicon_foxNews.xaml b/Morphic.Client/Assets/bar-icons/favicon_foxNews.xaml deleted file mode 100644 index a86c78f4..00000000 --- a/Morphic.Client/Assets/bar-icons/favicon_foxNews.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/globe.xaml b/Morphic.Client/Assets/bar-icons/globe.xaml deleted file mode 100644 index 303ec870..00000000 --- a/Morphic.Client/Assets/bar-icons/globe.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_abcNews.xaml b/Morphic.Client/Assets/bar-icons/logo_abcNews.xaml deleted file mode 100644 index 956f71bb..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_abcNews.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_alJazeera.jpg b/Morphic.Client/Assets/bar-icons/logo_alJazeera.jpg deleted file mode 100644 index f9ff023d..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_alJazeera.jpg and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_amazon.xaml b/Morphic.Client/Assets/bar-icons/logo_amazon.xaml deleted file mode 100644 index 1b8d6d11..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_amazon.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_amazonMusic.png b/Morphic.Client/Assets/bar-icons/logo_amazonMusic.png deleted file mode 100644 index b6f681bf..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_amazonMusic.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_aolOld.png b/Morphic.Client/Assets/bar-icons/logo_aolOld.png deleted file mode 100644 index f70bfa85..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_aolOld.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_bbc.xaml b/Morphic.Client/Assets/bar-icons/logo_bbc.xaml deleted file mode 100644 index db3f2b98..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_bbc.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_bestBuy.xaml b/Morphic.Client/Assets/bar-icons/logo_bestBuy.xaml deleted file mode 100644 index 214e35c7..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_bestBuy.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_bloomberg.xaml b/Morphic.Client/Assets/bar-icons/logo_bloomberg.xaml deleted file mode 100644 index 2d933343..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_bloomberg.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_box.xaml b/Morphic.Client/Assets/bar-icons/logo_box.xaml deleted file mode 100644 index 0870ee81..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_box.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_calculator.png b/Morphic.Client/Assets/bar-icons/logo_calculator.png deleted file mode 100644 index b78dd1bd..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_calculator.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_cbsNews.xaml b/Morphic.Client/Assets/bar-icons/logo_cbsNews.xaml deleted file mode 100644 index 82af5013..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_cbsNews.xaml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_chrome.png b/Morphic.Client/Assets/bar-icons/logo_chrome.png deleted file mode 100644 index e8eaa48b..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_chrome.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_cnbc.xaml b/Morphic.Client/Assets/bar-icons/logo_cnbc.xaml deleted file mode 100644 index ba3e437b..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_cnbc.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_cnn.xaml b/Morphic.Client/Assets/bar-icons/logo_cnn.xaml deleted file mode 100644 index fe912cc8..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_cnn.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_craigslist.xaml b/Morphic.Client/Assets/bar-icons/logo_craigslist.xaml deleted file mode 100644 index 3e1b8132..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_craigslist.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_deezer.xaml b/Morphic.Client/Assets/bar-icons/logo_deezer.xaml deleted file mode 100644 index 0470a9da..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_deezer.xaml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_disneyPlus.xaml b/Morphic.Client/Assets/bar-icons/logo_disneyPlus.xaml deleted file mode 100644 index 57a23e0f..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_disneyPlus.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_dropbox.xaml b/Morphic.Client/Assets/bar-icons/logo_dropbox.xaml deleted file mode 100644 index f26fa7fc..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_dropbox.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_drudgeReport.xaml b/Morphic.Client/Assets/bar-icons/logo_drudgeReport.xaml deleted file mode 100644 index c63f60d9..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_drudgeReport.xaml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_ebay.png b/Morphic.Client/Assets/bar-icons/logo_ebay.png deleted file mode 100644 index cfefe385..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_ebay.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_etsy.xaml b/Morphic.Client/Assets/bar-icons/logo_etsy.xaml deleted file mode 100644 index db57aaa0..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_etsy.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_facebook.xaml b/Morphic.Client/Assets/bar-icons/logo_facebook.xaml deleted file mode 100644 index 47046f14..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_facebook.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_firefox.png b/Morphic.Client/Assets/bar-icons/logo_firefox.png deleted file mode 100644 index 98bc7e49..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_firefox.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_forbes.png b/Morphic.Client/Assets/bar-icons/logo_forbes.png deleted file mode 100644 index 3717ad94..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_forbes.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_foxNews.xaml b/Morphic.Client/Assets/bar-icons/logo_foxNews.xaml deleted file mode 100644 index 24a0819e..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_foxNews.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_gmail.png b/Morphic.Client/Assets/bar-icons/logo_gmail.png deleted file mode 100644 index 7d150332..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_gmail.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_googleDrive.png b/Morphic.Client/Assets/bar-icons/logo_googleDrive.png deleted file mode 100644 index b2511a63..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_googleDrive.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_googleNews.png b/Morphic.Client/Assets/bar-icons/logo_googleNews.png deleted file mode 100644 index ca2e41c6..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_googleNews.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_huffpost.xaml b/Morphic.Client/Assets/bar-icons/logo_huffpost.xaml deleted file mode 100644 index 139122e5..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_huffpost.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_hulu.xaml b/Morphic.Client/Assets/bar-icons/logo_hulu.xaml deleted file mode 100644 index f08dd283..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_hulu.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_icloud.xaml b/Morphic.Client/Assets/bar-icons/logo_icloud.xaml deleted file mode 100644 index 784c1c21..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_icloud.xaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_iheartRadio.xaml b/Morphic.Client/Assets/bar-icons/logo_iheartRadio.xaml deleted file mode 100644 index 199b5b24..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_iheartRadio.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_imgur.xaml b/Morphic.Client/Assets/bar-icons/logo_imgur.xaml deleted file mode 100644 index 3e2d6136..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_imgur.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_instagram.png b/Morphic.Client/Assets/bar-icons/logo_instagram.png deleted file mode 100644 index 1cb1819a..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_instagram.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_itunes.png b/Morphic.Client/Assets/bar-icons/logo_itunes.png deleted file mode 100644 index db8465ea..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_itunes.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_kohls.xaml b/Morphic.Client/Assets/bar-icons/logo_kohls.xaml deleted file mode 100644 index ead8cf68..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_kohls.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_laTimes.xaml b/Morphic.Client/Assets/bar-icons/logo_laTimes.xaml deleted file mode 100644 index 4e396410..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_laTimes.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_linkedIn.xaml b/Morphic.Client/Assets/bar-icons/logo_linkedIn.xaml deleted file mode 100644 index b09b994e..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_linkedIn.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_macys.xaml b/Morphic.Client/Assets/bar-icons/logo_macys.xaml deleted file mode 100644 index b398d46f..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_macys.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_mail.jpg b/Morphic.Client/Assets/bar-icons/logo_mail.jpg deleted file mode 100644 index d71e59fa..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_mail.jpg and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_msaccess.xaml b/Morphic.Client/Assets/bar-icons/logo_msaccess.xaml deleted file mode 100644 index 694f7851..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msaccess.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_msedge.png b/Morphic.Client/Assets/bar-icons/logo_msedge.png deleted file mode 100644 index 18941eb8..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_msedge.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_msexcel.xaml b/Morphic.Client/Assets/bar-icons/logo_msexcel.xaml deleted file mode 100644 index 66610e57..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msexcel.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_msonenote.xaml b/Morphic.Client/Assets/bar-icons/logo_msonenote.xaml deleted file mode 100644 index 591c22fc..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msonenote.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_msoutlook.xaml b/Morphic.Client/Assets/bar-icons/logo_msoutlook.xaml deleted file mode 100644 index be498e3e..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msoutlook.xaml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_mspowerpoint.xaml b/Morphic.Client/Assets/bar-icons/logo_mspowerpoint.xaml deleted file mode 100644 index 2e98457e..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_mspowerpoint.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_msquickassist.png b/Morphic.Client/Assets/bar-icons/logo_msquickassist.png deleted file mode 100644 index 2b14bae7..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_msquickassist.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_msteams.xaml b/Morphic.Client/Assets/bar-icons/logo_msteams.xaml deleted file mode 100644 index fd555103..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msteams.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_msword.xaml b/Morphic.Client/Assets/bar-icons/logo_msword.xaml deleted file mode 100644 index 081b57bb..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_msword.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_nbcNews.xaml b/Morphic.Client/Assets/bar-icons/logo_nbcNews.xaml deleted file mode 100644 index 61659f9c..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_nbcNews.xaml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_netflix.png b/Morphic.Client/Assets/bar-icons/logo_netflix.png deleted file mode 100644 index 51bea609..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_netflix.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_newYorkTimes.xaml b/Morphic.Client/Assets/bar-icons/logo_newYorkTimes.xaml deleted file mode 100644 index defb8398..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_newYorkTimes.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_nextdoor.xaml b/Morphic.Client/Assets/bar-icons/logo_nextdoor.xaml deleted file mode 100644 index 640319d2..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_nextdoor.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_npr.xaml b/Morphic.Client/Assets/bar-icons/logo_npr.xaml deleted file mode 100644 index 54b7ea63..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_npr.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_onedrive.xaml b/Morphic.Client/Assets/bar-icons/logo_onedrive.xaml deleted file mode 100644 index c1b396ec..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_onedrive.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_opera.png b/Morphic.Client/Assets/bar-icons/logo_opera.png deleted file mode 100644 index eb545cf5..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_opera.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_outlook.xaml b/Morphic.Client/Assets/bar-icons/logo_outlook.xaml deleted file mode 100644 index ed1945c6..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_outlook.xaml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_pandora.jpg b/Morphic.Client/Assets/bar-icons/logo_pandora.jpg deleted file mode 100644 index 6dfd2418..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_pandora.jpg and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_pinterest.xaml b/Morphic.Client/Assets/bar-icons/logo_pinterest.xaml deleted file mode 100644 index 96262551..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_pinterest.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_reddit.xaml b/Morphic.Client/Assets/bar-icons/logo_reddit.xaml deleted file mode 100644 index 1ee76552..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_reddit.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_reuters.xaml b/Morphic.Client/Assets/bar-icons/logo_reuters.xaml deleted file mode 100644 index fa55edbf..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_reuters.xaml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_skype.xaml b/Morphic.Client/Assets/bar-icons/logo_skype.xaml deleted file mode 100644 index 68b69f98..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_skype.xaml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_soundcloud.xaml b/Morphic.Client/Assets/bar-icons/logo_soundcloud.xaml deleted file mode 100644 index d84f3bb6..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_soundcloud.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_spotify.xaml b/Morphic.Client/Assets/bar-icons/logo_spotify.xaml deleted file mode 100644 index 95f71999..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_spotify.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_target.xaml b/Morphic.Client/Assets/bar-icons/logo_target.xaml deleted file mode 100644 index f39a10b6..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_target.xaml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_theGuardian.xaml b/Morphic.Client/Assets/bar-icons/logo_theGuardian.xaml deleted file mode 100644 index 1971ed5f..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_theGuardian.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_theHill.jpeg b/Morphic.Client/Assets/bar-icons/logo_theHill.jpeg deleted file mode 100644 index 387e910d..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_theHill.jpeg and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_tidal.png b/Morphic.Client/Assets/bar-icons/logo_tidal.png deleted file mode 100644 index 00f57da2..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_tidal.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_tumblr.xaml b/Morphic.Client/Assets/bar-icons/logo_tumblr.xaml deleted file mode 100644 index de33575c..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_tumblr.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_twitter.xaml b/Morphic.Client/Assets/bar-icons/logo_twitter.xaml deleted file mode 100644 index 9cf25d59..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_twitter.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_usaToday.xaml b/Morphic.Client/Assets/bar-icons/logo_usaToday.xaml deleted file mode 100644 index 71c6b71d..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_usaToday.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_vimeo.xaml b/Morphic.Client/Assets/bar-icons/logo_vimeo.xaml deleted file mode 100644 index 4d6aebe0..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_vimeo.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_walmart.xaml b/Morphic.Client/Assets/bar-icons/logo_walmart.xaml deleted file mode 100644 index 21cdfb00..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_walmart.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_washingtonPost.xaml b/Morphic.Client/Assets/bar-icons/logo_washingtonPost.xaml deleted file mode 100644 index 5c4372bc..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_washingtonPost.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_wayfair.png b/Morphic.Client/Assets/bar-icons/logo_wayfair.png deleted file mode 100644 index 2cc568bf..00000000 Binary files a/Morphic.Client/Assets/bar-icons/logo_wayfair.png and /dev/null differ diff --git a/Morphic.Client/Assets/bar-icons/logo_wsj.xaml b/Morphic.Client/Assets/bar-icons/logo_wsj.xaml deleted file mode 100644 index 3d818fcd..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_wsj.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_yahoo.xaml b/Morphic.Client/Assets/bar-icons/logo_yahoo.xaml deleted file mode 100644 index 7c5b406a..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_yahoo.xaml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_yahoomail.xaml b/Morphic.Client/Assets/bar-icons/logo_yahoomail.xaml deleted file mode 100644 index 13972f76..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_yahoomail.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_youtube.xaml b/Morphic.Client/Assets/bar-icons/logo_youtube.xaml deleted file mode 100644 index 9aca5e05..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_youtube.xaml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Morphic.Client/Assets/bar-icons/logo_youtubeMusic.xaml b/Morphic.Client/Assets/bar-icons/logo_youtubeMusic.xaml deleted file mode 100644 index c3e689b7..00000000 --- a/Morphic.Client/Assets/bar-icons/logo_youtubeMusic.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/morphic-logo.xaml b/Morphic.Client/Assets/bar-icons/morphic-logo.xaml deleted file mode 100644 index d5ab7628..00000000 --- a/Morphic.Client/Assets/bar-icons/morphic-logo.xaml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/newspaper.xaml b/Morphic.Client/Assets/bar-icons/newspaper.xaml deleted file mode 100644 index fb530352..00000000 --- a/Morphic.Client/Assets/bar-icons/newspaper.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/bar-icons/window-maximize.xaml b/Morphic.Client/Assets/bar-icons/window-maximize.xaml deleted file mode 100644 index 9b48ed8b..00000000 --- a/Morphic.Client/Assets/bar-icons/window-maximize.xaml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Morphic.Client/Assets/morphic-icon.ico b/Morphic.Client/Assets/morphic-icon.ico deleted file mode 100644 index 39f8ed22..00000000 Binary files a/Morphic.Client/Assets/morphic-icon.ico and /dev/null differ diff --git a/Morphic.Client/Backups.cs b/Morphic.Client/Backups.cs deleted file mode 100644 index 542732f7..00000000 --- a/Morphic.Client/Backups.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace Morphic.Client -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text.Json; - using System.Threading.Tasks; - using Config; - using Core; - using Microsoft.Extensions.Logging; - using Service; - using Path = System.IO.Path; - - public class Backups - { - private readonly MorphicSession morphicSession; - private readonly ILogger logger; - private readonly IServiceProvider serviceProvider; - - public static string BackupDirectory => AppPaths.GetUserLocalConfigDir("backups"); - private static readonly string BackupExtension = ".preferences"; - - public Backups(MorphicSession morphicSession, ILogger logger, IServiceProvider serviceProvider) - { - this.morphicSession = morphicSession; - this.logger = logger; - this.serviceProvider = serviceProvider; - } - - - /// - /// Stores some preferences to a file, for a backup. - /// - /// Short description for display (one or two words, file-safe characters) - /// The preferences to store - null to capture them. - public async Task Store(Preferences? preferences = null) - { - this.logger.LogInformation("Making backup"); - if (preferences is null) - { - preferences = new Preferences(); - // NOTE: we are not returning any kind of error if the preferences were not captured - _ = await this.morphicSession.Solutions.CapturePreferencesAsync(preferences); - } - - string json = JsonSerializer.Serialize(preferences); - string filename = DateTime.Now.ToString("yyyy-MM-dd_HH.mm.ss") + BackupExtension; - string path = Path.Combine(BackupDirectory, filename); - - Directory.CreateDirectory(BackupDirectory); - await File.WriteAllTextAsync(path, json); - - this.logger.LogInformation($"Stored backup to {path}"); - } - - /// - /// Gets the list of backup files. - /// - /// filename:date - public IDictionary GetBackups() - { - Dictionary backups = new Dictionary(); - - if (Directory.Exists(BackupDirectory)) - { - foreach (string path in Directory.EnumerateFiles(BackupDirectory, "*" + BackupExtension) - .OrderBy(f => f)) - { - // Get the date from the filename. - string dateString = Path.ChangeExtension(Path.GetFileName(path), null); - if (DateTime.TryParse(dateString.Replace('_', ' ').Replace('.', ':'), out DateTime date)) - { - backups.Add(path, date.ToString("g")); - } - } - } - - return backups; - } - - /// - /// Applies a back-up. - /// - /// The backup file. - public async Task Apply(string path) - { - string json = await File.ReadAllTextAsync(path); - JsonSerializerOptions options = new JsonSerializerOptions(); - options.Converters.Add(new JsonElementInferredTypeConverter()); - this.morphicSession.Preferences = JsonSerializer.Deserialize(json, options); - await this.morphicSession.ApplyAllPreferences(); - } - } -} diff --git a/Morphic.Client/Bar/AppFocus.cs b/Morphic.Client/Bar/AppFocus.cs deleted file mode 100644 index 10af6b04..00000000 --- a/Morphic.Client/Bar/AppFocus.cs +++ /dev/null @@ -1,121 +0,0 @@ -namespace Morphic.Client.Bar -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Windows; - using System.Windows.Threading; - using UI.AppBarWindow; - - public class AppFocus - { - public static AppFocus Current { get; } = new AppFocus(); - - /// - /// true if the current application is active. - /// - public bool IsActive { get; private set; } - - /// The mouse has entered any window belonging to the application. - public event EventHandler? MouseEnter; - /// The mouse has left any window belonging to the application. - public event EventHandler? MouseLeave; - - public event EventHandler? Activated; - public event EventHandler? Deactivated; - - protected AppFocus() - { - App.Current.Activated += (o, args) => this.Activated?.Invoke(o, args); - App.Current.Activated += (o, args) => this.Deactivated?.Invoke(o, args); - this.Activated += (sender, args) => this.IsActive = true; - this.Deactivated += (sender, args) => this.IsActive = false; - } - - // The mouse is over any window in mouseOverWindows - private bool mouseOver; - - // The windows where the mouse-over status is needed. - private readonly List mouseOverWindows = new List(); - private DispatcherTimer? mouseTimer; - - /// - /// Register interest in observing the mouse-over state of a window. - /// - /// - public void AddMouseOverWindow(Window window) - { - this.mouseOverWindows.Add(window); - window.MouseEnter += this.CheckMouseOver; - window.MouseLeave += this.CheckMouseOver; - } - - private void CheckMouseOver(object? sender, EventArgs e) - { - if (this.mouseOverWindows.Count == 0) - { - return; - } - - bool isOver = false; - IEnumerable windows = this.mouseOverWindows.Where(w => w.IsVisible && w.Opacity > 0); - - Point? cursor = null; - - // Window.IsMouseOver is false if the mouse is over the window border, check if that's the case. - foreach (Window window in windows) - { - if (window.IsMouseOver) - { - isOver = true; - break; - } - - cursor ??= PresentationSource.FromVisual(window)?.CompositionTarget.TransformFromDevice - .Transform(WindowMovement.GetCursorPos()); - - if (cursor is not null) - { - System.Windows.Rect rc = window.GetRect(); - rc.Inflate(10, 10); - if (rc.Contains(cursor.Value)) - { - isOver = true; - if (this.mouseTimer is null) - { - // Keep an eye on the current position. - this.mouseTimer = new DispatcherTimer(DispatcherPriority.Input) - { - Interval = TimeSpan.FromMilliseconds(100), - }; - this.mouseTimer.Tick += this.CheckMouseOver; - this.mouseTimer.Start(); - } - - break; - } - } - } - - if (!isOver) - { - this.mouseTimer?.Stop(); - this.mouseTimer = null; - } - - if (this.mouseOver != isOver) - { - this.mouseOver = isOver; - if (isOver) - { - this.MouseEnter?.Invoke(sender, new EventArgs()); - } - else - { - this.MouseLeave?.Invoke(sender, new EventArgs()); - } - } - } - - } -} diff --git a/Morphic.Client/Bar/BarData.md b/Morphic.Client/Bar/BarData.md deleted file mode 100644 index 89f62832..00000000 --- a/Morphic.Client/Bar/BarData.md +++ /dev/null @@ -1,519 +0,0 @@ -# Data structure for Morphic Bar - -## `Bar` - -This is what the client can handle for the bar, which is a super-set of what is provided by the web app. - -There is initial data, in `default-bar.json5`. This is loaded first, then the data from the web app is merged over it. - -Not all fields are required, as the client will already have its own predefined defaults. Assume fields to be optional, unless stated. - -```js -Bar = { - // Bar identifier - id: "bar1", - // Bar name - name: "Example bar", - - // Initial position - position: { - // Dock it to an edge, reserving desktop space - docked: "left", // "left", "right", "top", "bottom", "none" (default), or "disable". - - horizontal: false, // true for horizontal orientation. - restricted: false, // true to restrict the position to just the corners - - // Position of the bar. Can be "Left"/"Top", "Middle", "Right"/"Bottom", a number, or a percentage. - // Numbers or percentages can be negative (including -0), meaning distance from the right. - // Percentages specify the position of the middle of the bar. - // Ignored if `docked` is used. - x: "50%", - y: "Bottom", - - // Position of the secondary bar, relative to the primary bar. Same syntax as `x`/`y` above. - // (can be split with `secondaryX` and `secondaryY`) - secondary: "Middle", - - // Position of the expander button (the thing that opens the secondary bar) - // (can be split with `expanderX` and `expanderY`) - expander: "Middle", - // What the position in `expander` is relative to. - // "primary", "secondary", or "both" (secondary if the secondary bar is open, otherwise primary) - expanderRelative: "Both", - }, - - // Settings for the secondary bar - secondaryBar: { - // Close the secondary bar when another application takes focus. - autohide: true, - // Hide the expander button when another application takes focus (shown on mouse-over). - autohideExpander: false - }, - - // Size of everything - scale: 1, - - // What happens when all buttons do not fit. - // "resize": shrinks some items until it fits - // "wrap": Adds another column - // "secondary": Move over-flow items to the secondary bar. - // "hide": Do nothing. - overflow: "resize", - - // Bar theme - theme: {Theme}, - - // Theme for bar items - itemTheme: {ItemTheme}, - - // The bar items - items: [ - {BarItem} - ], - - sizes: { - // Padding between edge of bar and items. - windowPadding: "10 15", - // Spacing between items. - itemSpacing: 10, - // Item width. - itemWidth: 100, - // Maximum Button Item title lines. - buttonTextLines: 2, - // Button Item padding between edge and title. And for the top, between circle and title. - buttonPadding: "10", - // Button Item circle image diameter (a fraction relative to the itemWidth). - buttonCircleDiameter: 0.66, - // Button Item circle overlap with rectangle (a fraction relative to buttonImageSize). - buttonImageOverlap: 0.33, - buttonFontSize: 14, - buttonFontWeight: "normal", - circleBorderWidth: 2, - buttonCornerRadius: 10 - } -} -``` - -## `BarItem` - -Describes an individual bar item. - -```js -BarItem = { - // email|calendar|videocall|photos|... - // Currently ignored by the client - category: "calendar", - - // unique identifier (currently ignored by client) - id: "calendar-button", - - // `true` if the item is shown on the primary bar. `false` to show on the secondary bar. - is_primary: true, - - // `true` to never move this item to the secondary bar (for Bar.overflow == "secondary") - no_overflow: false, - - // Per-button theme, overrides the `Bar.itemTheme` field from above. - // If unset, this is generated using `configuration.color` - theme: {Theme}, - - // `true` to not show this button. While it's expected that the client will only receive the items which should be - // shown, this field provides the ability to show or hide items depending on the platform, using the platform - // identifier, described later. For example, `hidden$win: true` will make the item only available for macOS. - hidden: false, - - // Items are sorted by this (higher values at the top). - priority: 0, - - // The kind of item (see Item kinds below) [REQUIRED] - // "link", "application", "action" - kind: "link", - - // "button" (default), "image", "multi" (see Widgets below) - widget: "button", - - // Specific to the item kind. - configuration: { - // ... - } -} -``` - -## Button items - -```js -/** @mixes BarItem */ -ButtonItem = { - kind: "", - configuration: { - // Displayed on the button [REQUIRED] - label: "Calendar", - - // Tooltip. - tooltipHeader: "Open the calendar", - // More details. - tooltip: "Displays your google calendar", - - // Automation UI name - this is used by narrator. default is the label. - uiName: "Calendar", - - // local/remote url of the icon. For values without a directory, a matching file in ./Assets/bar-icons/`) is - // discovered. If this value is omitted (or not working), an image is detected depending on the kind of item: - // - link: favicon of the site. - // - application: The application icon. - image_url: "calendar.png", - - // Item color (overrides BarItem.theme, generates the different shades for the states) - color: '#002957', - - // Size of the item. "textonly", "small", "medium", or "large" (default) - size: "large", - - // Context menu - menu: {ContextMenu} - - } -} -``` - -### `kind = "link"` - -Opens a web page. - -```js -/** @extends ButtonItem */ -LinkButton = { - kind: "link", - /** @mixes LinkAction */ - configuration: { - url: "https://example.com" - } -} -``` - -### `kind = "application"` - -Opens an application. - -```js -/** @extends ButtonItem */ -ApplicationButton = { - kind: "application", - /** @mixes ApplicationAction */ - configuration: { - // Executable name (or full path). Full path is discovered via `App Paths` registry or the PATH environment variable. - // To pass arguments, surround the executable with quotes and append the arguments after (or use the args field) - exe: "notepad.exe", - // Arguments to pass to the process - args: [ "arg1", "arg2" ], - // Extra environment variables - env: { - name: "value" - }, - // Always start a new instance (otherwise, activate the existing instance if one is found) - newInstance: true, - // Initial state of the window (not all apps honour this) - windowStyle: "normal" // "normal" (default), "maximized", "minimized" or "hidden" - } -} -``` - -Or, run a default application. Use the `default` field to identify an entry in [`default-apps.json5`](#default-appsjson5). - -```js -/** @extends ButtonItem */ -ApplicationButton = { - kind: "application", - /** @mixes ApplicationAction */ - configuration: { - // The key to lookup in default-apps.json5. - default: "email", - } -} -``` - -### `kind = "internal"` - -Invokes a built-in routine. - -```js -/** @extends ButtonItem */ -InternalButton = { - kind: "internal", - /** @mixes InternalAction */ - configuration: { - // Name of the internal function. - function: "fname", - // Arguments to pass. - args: ["a1", "a2"] - } -} -``` - -### `kind = "shellExec"` - -Executes a command via the windows shell (similar to the `start` command or the run dialog box). - -```js -/** @extends ButtonItem */ -ShellExecButton = { - kind: "shellExec", - /** @mixes ApplicationAction */ - configuration: { - // The command - default: "ms-settings:" - } -} -``` - -### `kind = "setting"` - -Changes a setting. Currently, only boolean or integer settings are supported. - -```js -/** @extends ButtonItem */ -SettingButton = { - kind: "setting", - /** @mixes SettingAction */ - configuration: { - // The setting path - settingId: "com.microsoft.windows.magnifier/enabled" - } -} -``` - - -### `kind = "action"` - -This performs a lookup of an `action` object in [`presets.json5`](#presetsjson5), using `configuration.identifier`. -The object in `presets.json5` will be merged onto this one. - -This allows for a richer set of data than what the web app provides. - -```js -/** @extends ButtonItem */ -ActionButton = { - kind: "action", - /** @mixes PresetAction */ - configuration: { - identifier: "example-action" - } -} -``` - -## Widgets - -### `widget = "button"` - -Standard button. - -### `widget = "image"` - -Behaves like a button, but only displays an image. Used for the logo button. - -```js -/** @extends ButtonItem */ -ImageItem = { - widget: "image" -} -``` - -### `widget = "multi"` - -Displays multiple buttons in a single item. Used by the settings items. - -```js -/** @extends BarItem */ -MultiButtonItem = { - widget: "multi", - configuration: { - // How the buttons are interracted with via the keyboard: "buttons", "additive", "toggle", "auto" (default) - // "additive" and "toggle" cause the bar item to behave as a single control (for keyboard navigation), and - // the button pair is accessed via -/+ keys. - // For "buttons", each button is a tab stop. "auto" (default) will detect, based on the button names. - type: "auto", - buttons: { - // First button - button1: { - // Display text - label: "day", - // A value that replaces "{button}" in any action payload (eg, `exe: "app.exe {button}"`). - value: "b1", - - tooltip: "Tooltip header|Tooltip text", - uiName: "Button one", - menu: {ContextMenu} - }, - // next button - button2: { - label: "night" - }, - // ... - } - } -} -``` - -Example: - -```json5 - { - // Pass either ^c or ^v to the `sendKeys` internal function. - kind: "internal", - widget: "multi", - configuration: { - defaultLabel: "Clipboard", - function: "sendKeys", - args: { - keys: "{button}" - }, - buttons: { - copy: { - label: "Copy", - value: "^c" - }, - paste: { - label: "Paste", - value: "^v" - } - } - } - } -``` - - -## `Theme` - -Used to specify the theme of the bar or an item. - -```js -Theme = { - color: "white", - background: "#002957", - // Only used by bar items - borderColor: "#ff0", - focusDotColor: "#000", - borderSize: 2 -} -``` - -## `ItemTheme : Theme` - -```js -/** @extends Theme */ -ItemTheme = { - // from Theme - color: "white", - background: "#002957", - borderColor: "#ff0", - focusDotColor: "#000", - borderSize: 2, - - // Optional, will use the above style. - hover: {Theme}, // Mouse is over the item. - focus: {Theme}, // Item has keyboard focus. - active: {Theme} // Item is being clicked (mouse is down). -} -``` - -## `ContextMenu` - -```js -ContextMenu = { - "setting": "easeofaccess-colorfilter", - "learn": "color", - "demo": "color" -} - -``` - -## presets.json5 - -This file contains additional data for certain bar items. This allows for additional bar information provided by the client. -A bar item, from the web app, which points to an object in this file will have this object merged onto it. - -For bar items with `kind = "action"`, the value of `configuration.identifier` identifies a key in `actions`. -For bar items with `kind = "application"`, the value of `configuration.default` identifies a key in `defaults`. - -```js -presets.json5 = { - actions: { - "identifier": {BarItem}, - - // start task manager - "taskManager": { - kind: "application", - configuration: { - exe: "taskmgr.exe" - } - }, - - // Example: invoke an internal function - "example": { - kind: "internal", - configuration: { - function: "hello" - } - }, - - // Real example - "high-contrast": { - kind: "application", - widget: "multi", - configuration: { - defaultLabel: "High-Contrast", - exe: "sethc.exe", - args: [ "{button}" ], - buttons: { - on: { - label: "On", - value: "100" - }, - off: { - label: "Off", - value: "1" - } - } - } - } - }, - - defaults: { - // Same as actions. - "identifier": {BarItem}, - - "email": { - configuration: { - exe: "mailto:" - } - } - } -} -``` - -## Cross-platform - -All fields in the bar json and `presets.json5` can be suffixed with an OS identifier (`$mac` or `$win`), which will take precedence over the non-suffixed field. This pre-processing would be done on the client. - -examples: - -```js -[ - { - command: "default command", - command$mac: "macOS command", - - label$win: "on windows", - labelText: "not windows" - }, - { - command: "default command", - command$win: "windows command" - }, - { - command: "default command (ignored)", - command$win: "windows command", - command$mac: "macOS command" - }, -] -``` diff --git a/Morphic.Client/Bar/BarImages.cs b/Morphic.Client/Bar/BarImages.cs deleted file mode 100644 index 32be75b8..00000000 --- a/Morphic.Client/Bar/BarImages.cs +++ /dev/null @@ -1,337 +0,0 @@ -namespace Morphic.Client.Bar -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text.RegularExpressions; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Media; - using System.Windows.Media.Imaging; - using System.Windows.Resources; - using System.Xml; - using Config; - - public class BarImages - { - /// - /// Gets the full path to a bar icon in the assets directory, based on its name (with or without the extension). - /// - /// Name of the icon. - /// - public static string? GetBarIconFile(string name) - { - var translatedName = BarImages.TranslateImageUrlToFileName(name); - if (translatedName is not null) - { - name = translatedName; - } - - string safe = new Regex(@"\.\.|[^-a-zA-Z0-9./]+", RegexOptions.Compiled) - .Replace(name, "_") - .Trim('/') - .Replace('/', Path.DirectorySeparatorChar); - string assetFile = AppPaths.GetAssetFile("bar-icons\\" + safe); - string[] extensions = { "", ".xaml", ".png", ".ico", ".jpg", ".jpeg", ".gif" }; - - string? foundFile = extensions.Select(extension => assetFile + extension) - .FirstOrDefault(File.Exists); - - return foundFile; - } - - // NOTE: the image_url values we get back from the v1 API do not always represent the filename, so we need to map them here - // in the (very-near-term) future, we must standardize on URLs or another form via the API; manual mapping is not sustainable - public static string? TranslateImageUrlToFileName(string? imageUrl) { - switch (imageUrl) { - case "abcnews": - return "logo_abcNews"; - case "aljazeera": - return "logo_alJazeera"; - case "amazon": - return "logo_amazon"; - case "amazonmusic": - return "logo_amazonMusic"; - case "aolold": - return "logo_aolOld"; - case "bbc": - return "logo_bbc"; - case "bestbuy": - return "logo_bestBuy"; - case "bloomberg": - return "logo_bloomberg"; - case "box": - return "logo_box"; - case "calculator": - return "logo_calculator"; - case "calendar$calendar": - return "calendar"; - case "cbsnews": - return "logo_cbsNews"; - case "chrome": - return "logo_chrome"; - case "cnbc": - return "logo_cnbc"; - case "cnn": - return "logo_cnn"; - case "craigslist": - return "logo_craigslist"; - case "deezer": - return "logo_deezer"; - case "disneyplus": - return "logo_disneyPlus"; - case "dropbox": - return "logo_dropbox"; - case "drudgereport": - return "logo_drudgeReport"; - case "ebay": - return "logo_ebay"; - case "etsy": - return "logo_etsy"; - case "email$envelope": - return "envelope"; - case "email$envelopeopen": - return "envelope-open"; - case "email$envelopeopentext": - return "envelope-open-text"; - case "email$envelopeoutline": - return "envelope-outline"; - case "email$envelopeoutlineopen": - return "envelope-outline-open"; - case "facebook": - return "logo_facebook"; - case "faviconfoxnews": - return "favicon_foxNews"; - case "firefox": - return "logo_firefox"; - case "forbes": - return "logo_forbes"; - case "foxnews": - return "logo_foxNews"; - case "gmail": - return "logo_gmail"; - case "googledrive": - return "logo_googleDrive"; - case "googlenews": - return "logo_googleNews"; - case "huffpost": - return "logo_huffpost"; - case "hulu": - return "logo_hulu"; - case "icloud": - return "logo_icloud"; - case "news$newspaper": - return "newspaper"; - case "iheartradio": - return "logo_iheartRadio"; - case "imgur": - return "logo_imgur"; - case "instagram": - return "logo_instagram"; - case "itunes": - return "logo_itunes"; - case "kohls": - return "logo_kohls"; - case "latimes": - return "logo_laTimes"; - case "linkedin": - return "logo_linkedIn"; - case "macys": - return "logo_macys"; - case "mail": - return "logo_mail"; - case "msaccess": - return "logo_msaccess"; - case "msedge": - return "logo_msedge"; - case "msexcel": - return "logo_msexcel"; - case "msonenote": - return "logo_msonenote"; - case "msoutlook": - return "logo_msoutlook"; - case "mspowerpoint": - return "logo_mspowerpoint"; - case "msquickassist": - return "logo_msquickassist"; - case "msteams": - return "logo_msteams"; - case "msword": - return "logo_msword"; - case "nbcnews": - return "logo_nbcNews"; - case "netflix": - return "logo_netflix"; - case "nextdoor": - return "logo_nextdoor"; - case "newyorktimes": - return "logo_newYorkTimes"; - case "npr": - return "logo_npr"; - case "onedrive": - return "logo_onedrive"; - case "opera": - return "logo_opera"; - case "outlook": - return "logo_outlook"; - case "pandora": - return "logo_pandora"; - case "pinterest": - return "logo_pinterest"; - case "reddit": - return "logo_reddit"; - case "reuters": - return "logo_reuters"; - case "skype": - return "logo_skype"; - case "spotify": - return "logo_spotify"; - case "soundcloud": - return "logo_soundcloud"; - case "target": - return "logo_target"; - case "theguardian": - return "logo_theGuardian"; - case "thehill": - return "logo_theHill"; - case "tidal": - return "logo_tidal"; - case "tumblr": - return "logo_tumblr"; - case "twitter": - return "logo_twitter"; - case "usatoday": - return "logo_usaToday"; - case "vimeo": - return "logo_vimeo"; - case "walmart": - return "logo_walmart"; - case "washingtonpost": - return "logo_washingtonPost"; - case "wayfair": - return "logo_wayfair"; - case "windowmaximize": - return "window-maximize"; - case "wsj": - return "logo_wsj"; - case "yahoo": - return "logo_yahoo"; - case "yahoomail": - return "logo_yahoomail"; - case "youtube": - return "logo_youtube"; - case "youtubemusic": - return "logo_youtubeMusic"; - case null: - default: - return imageUrl; - } - } - - - /// - /// Creates an image source from a local image. - /// - /// The path to the image, or the name of the icon in the assets directory. - /// The color, for monochrome vectors. - /// null if the image is not supported. - public static ImageSource? CreateImageSource(string imagePath, Color? color = null) - { - ImageSource? result; - - // Attempt to load a bitmap image. - ImageSource? TryBitmap() - { - try - { - BitmapImage image = new BitmapImage(); - image.BeginInit(); - image.CacheOption = BitmapCacheOption.OnLoad; - image.UriSource = new Uri(imagePath); - image.EndInit(); - return image; - } - catch (Exception e) when (e is NotSupportedException || e is XmlException) - { - return null; - } - } - - if ((imagePath.Contains('/') == false) && (imagePath.Contains('\\') == false)) - { - imagePath = GetBarIconFile(imagePath) ?? imagePath; - } - - result = TryBitmap(); - - return result; - } - - /// - /// Replaces the brushes used in a monochrome drawing with a new one, which can be set to a specific colour. - /// - /// The drawing to change. - /// The new colour to set (if brush is null). - /// The brush to use. - /// The brush used (null if the drawing isn't monochrome). - public static SolidColorBrush? ChangeDrawingColor(Drawing drawing, Color color, SolidColorBrush? brush = null) - { - List? geometryDrawings; - - // Get all the geometries in the drawing. - if (drawing is DrawingGroup drawingGroup) - { - geometryDrawings = GetDrawings(drawingGroup).OfType().ToList(); - } - else - { - geometryDrawings = new List(); - if (drawing is GeometryDrawing gd) - { - geometryDrawings.Add(gd); - } - } - - // If there's only 1 colour, it's mono. - bool mono = geometryDrawings.Count > 0 - && geometryDrawings - .Select(gd => gd.Brush) - .OfType() - .Where(b => b.Opacity > 0) - .Select(b => b.Color) - .Where(c => c.A != 0) - .Distinct() - .Count() == 1; - - if (!mono) - { - return null; - } - else - { - brush ??= new SolidColorBrush(color); - geometryDrawings.ForEach(gd => - { - if (gd.Brush is SolidColorBrush && gd.Brush.Opacity > 0) - { - gd.Brush = brush; - } - }); - return brush; - } - } - - /// - /// Gets all drawings within a drawing group. - /// - /// - /// - private static IEnumerable GetDrawings(DrawingGroup drawingGroup) - { - return drawingGroup.Children.OfType() - .SelectMany(GetDrawings) - .Concat(drawingGroup.Children.OfType()); - } - } -} diff --git a/Morphic.Client/Bar/BarManager.cs b/Morphic.Client/Bar/BarManager.cs deleted file mode 100644 index c7c12e87..00000000 --- a/Morphic.Client/Bar/BarManager.cs +++ /dev/null @@ -1,416 +0,0 @@ -// BarManager.cs: Loads and shows bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - - -namespace Morphic.Client.Bar -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.IO; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Threading.Tasks; - using System.Windows; - using Config; - using Core; - using Core.Community; - using Data; - using Dialogs; - using Microsoft.Extensions.Logging; - using Service; - using UI; - using MessageBox = System.Windows.Forms.MessageBox; - using SystemJson = System.Text.Json; - - /// - /// Looks after the bar. - /// - public class BarManager : INotifyPropertyChanged - { - private PrimaryBarWindow? barWindow; - private ILogger Logger => App.Current.Logger; - - public event EventHandler? BarLoaded; - public event EventHandler? BarUnloaded; - - private bool firstBar = true; - - public bool BarVisible => this.barWindow?.Visibility == Visibility.Visible; - - public BarManager() - { - } - - public bool BarIsLoaded { get; private set; } = false; - - /// - /// Show a bar that's already loaded. - /// - public void ShowBar() - { - if (this.barWindow is not null) - { - AppOptions.Current.MorphicBarIsVisible = true; - this.barWindow.Visibility = Visibility.Visible; - this.barWindow.Focus(); - } - } - - public void HideBar() - { - AppOptions.Current.MorphicBarIsVisible = false; - this.barWindow?.Hide(); - this.barWindow?.OtherWindow?.Hide(); - } - - /// - /// Closes the bar. - /// - public void CloseBar() - { - this.BarIsLoaded = false; - - if (this.barWindow is not null) - { - this.OnBarUnloaded(this.barWindow); - BarData bar = this.barWindow.Bar; - this.barWindow.IsClosing = true; - this.barWindow.Close(); - this.barWindow = null; - bar.Dispose(); - } - } - - public BarWindow CreateBarWindow(BarData bar) - { - this.barWindow = new PrimaryBarWindow(bar); - this.barWindow.BarLoaded += this.OnBarLoaded; - this.barWindow.IsVisibleChanged += this.BarWindowOnIsVisibleChanged; - - bool showMorphicBar = false; - if (AppOptions.Current.AutoShow == true) - { - showMorphicBar = true; - } - if (this.firstBar == false) - { - showMorphicBar = true; - } - if (AppOptions.Current.MorphicBarIsVisible == true) - { - showMorphicBar = true; - } - if (ConfigurableFeatures.MorphicBarVisibilityAfterLogin is not null) - { - switch (ConfigurableFeatures.MorphicBarVisibilityAfterLogin.Value) - { - case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Show: - showMorphicBar = true; - break; - case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Restore: - // if the bar has not been shown before, show it now; if it has been shown/hidden before, use the last known visibility state - showMorphicBar = AppOptions.Current.MorphicBarIsVisible ?? true; - break; - case ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption.Hide: - showMorphicBar = false; - break; - } - } - - // if we were started up manually, always show the MorphicBar - if (Environment.GetCommandLineArgs().Contains("--run-after-login") == false) - { - showMorphicBar = true; - } - - if (showMorphicBar == true) - { - AppOptions.Current.MorphicBarIsVisible = true; - this.barWindow.Show(); - } - - this.firstBar = false; - return this.barWindow; - } - - private void BarWindowOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) - { - this.OnPropertyChanged(nameof(this.BarVisible)); - } - - /// - /// Called when a bar has loaded. - /// - protected virtual void OnBarLoaded(object sender, EventArgs? args = null) - { - if (sender is PrimaryBarWindow window) - { - this.BarLoaded?.Invoke(this, new BarEventArgs(window)); - } - } - - /// - /// Called when a bar has closed. - /// - protected virtual void OnBarUnloaded(object sender, EventArgs? args = null) - { - if (sender is PrimaryBarWindow window) - { - this.BarUnloaded?.Invoke(this, new BarEventArgs(window)); - } - } - - #region DataLoading - private void OnBarOnReloadRequired(object? sender, EventArgs args) - { - if (sender is BarData bar) - { - string source = bar.Source; - - this.CloseBar(); - this.LoadFromBarJson(source); - } - } - - public BarData? LoadBasicMorphicBar() - { - var result = LoadFromBarJson(AppPaths.GetConfigFile("basic-bar.json5", true)); - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - return result; - } - - /// - /// Loads and shows a bar. - /// - /// JSON file containing the bar data. - /// The file content (if it's already loaded). - /// - public BarData? LoadFromBarJson(string path, string? content = null, IServiceProvider? serviceProvider = null) - { - if (this.firstBar && AppOptions.Current.Launch.BarFile is not null) - { - path = AppOptions.Current.Launch.BarFile; - } - - BarData? bar = null; - try - { - bar = BarData.Load(serviceProvider ?? App.Current.ServiceProvider, path, content); - } - catch (Exception e) when (!(e is OutOfMemoryException)) - { - this.Logger.LogError(e, "Problem loading the bar."); - } - - if (this.barWindow is not null) - { - this.CloseBar(); - } - - this.BarIsLoaded = true; - - if (bar is not null) - { - // if any of the items are application actions and the application isn't available, remove the button from the items collection - var index = 0; - while (index < bar.AllItems.Count) - { - var actionAsApplicationAction = bar.AllItems[index].Action as Data.Actions.ApplicationAction; - if (actionAsApplicationAction is not null) - { - if (actionAsApplicationAction.IsAvailable == false) - { - bar.AllItems.RemoveAt(index); - continue; - } - } - - index += 1; - } - - this.CreateBarWindow(bar); - bar.ReloadRequired += this.OnBarOnReloadRequired; - } - - return bar; - } - - /// - /// Loads the bar for the given session. If the user is a member of several, either the last one is used, - /// or a selection dialog is presented. - /// - /// The current session. - /// Force this community to show. - public async Task LoadSessionBarAsync(MorphicSession session, string communityId, string? morphicbarId) - { - if (this.firstBar && AppOptions.Current.Launch.BarFile is not null) - { - this.LoadFromBarJson(AppOptions.Current.Launch.BarFile); - return; - } - - this.Logger.LogInformation($"Loading a bar ({session.Communities.Length} communities)"); - - UserBar? bar; - - UserCommunity? community = null; - UserBar? userBar = null; - - //if (session.Communities.Length == 0) - //{ - // MessageBox.Show("You are not part of a Morphic community yet.", "Morphic"); - //} - //else if (session.Communities.Length == 1) - //{ - // community = session.Communities.First(); - //} - //else - //{ - // The user is a member of multiple communities. - - //// See if any membership has changed - //bool changed = session.Communities.Length != lastCommunities.Length - // || !session.Communities.Select(c => c.Id).OrderBy(id => id) - // .SequenceEqual(lastCommunities.OrderBy(id => id)); - - if (/*!changed &&*/ communityId is not null) - { - community = session.Communities.FirstOrDefault(c => c.Id == communityId); - } - - //if (community is null) - //{ - // this.Logger.LogInformation("Showing community picker"); - - // // Load the bars while the picker is shown - // Dictionary> bars = - // session.Communities.ToDictionary(c => c.Id, c => session.GetBar(c.Id)); - - // // Show the picker - // CommunityPickerWindow picker = new CommunityPickerWindow(session.Communities); - // bool gotCommunity = picker.ShowDialog() == true; - // community = gotCommunity ? picker.SelectedCommunity : null; - - // if (community is not null) - // { - // userBar = await bars[community.Id]; - // } - //} - //} - - if (community is not null) - { - var legacyBars = await session.GetBarsAsync(community.Id); - foreach (var legacyBar in legacyBars) { - var useThisBar = false; - if (morphicbarId is null) - { - // if the user selected this community id instead of a specific morphicbar (Morphic v1.0-v1.2), then use the first bar - useThisBar = true; - } - else if (legacyBar.Id == morphicbarId) - { - // if the user previously selected this specific morphicbar, use it - useThisBar = true; - } - - if (useThisBar == true) - { - // OBSERVATION: not sure why the "??=" (userBar is null) check is done here; this logic seems brittle - if (userBar is null) - { - userBar = legacyBar; - break; - } - } - } - // NOTE: if the morphicbar was not found, we do not set it to null (to remain consistent with previous code logic) - - // added to protect against not finding the specific community bar - if (userBar is not null) - { - this.Logger.LogInformation($"Showing bar for community {community.Id} {community.Name}"); - string barJson = this.GetUserBarJson(userBar); - BarData? barData = this.LoadFromBarJson(userBar.Id, barJson); - if (barData is not null) - { - barData.CommunityId = community.Id; - } - - AppOptions.Current.LastCommunity = community?.Id; - AppOptions.Current.LastMorphicbarId = userBar.Id; - } - else - { - // if the community or the specific community bar could not be found, show the Basic MorphicBar instead - this.LoadBasicMorphicBar(); - - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - } - } - else - { - // if the community could not be found, show the Basic MorphicBar instead - this.LoadBasicMorphicBar(); - - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - } - } - - /// - /// Gets the json for a , so it can be loaded with a better deserialiser. - /// - /// Bar data object from Morphic.Core - private string GetUserBarJson(UserBar userBar) - { - // Serialise the bar data so it can be loaded with a better deserialiser. - SystemJson.JsonSerializerOptions serializerOptions = new SystemJson.JsonSerializerOptions(); - serializerOptions.Converters.Add(new JsonElementInferredTypeConverter()); - serializerOptions.Converters.Add( - new SystemJson.Serialization.JsonStringEnumConverter(SystemJson.JsonNamingPolicy.CamelCase)); - string barJson = SystemJson.JsonSerializer.Serialize(userBar, serializerOptions); - - // Dump to a file, for debugging. - string barFile = AppPaths.GetConfigFile("last-bar.json5"); - File.WriteAllText(barFile, barJson); - - return barJson; - } - - #endregion - - #region INotifyPropertyChanged - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - #endregion - - } - - public class BarEventArgs : EventArgs - { - public BarEventArgs(PrimaryBarWindow window) - { - this.Window = window; - this.Bar = this.Window.Bar; - } - - public BarData Bar { get; private set; } - public PrimaryBarWindow Window { get; private set; } - - } -} diff --git a/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs b/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs deleted file mode 100644 index 6014277c..00000000 --- a/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs +++ /dev/null @@ -1,743 +0,0 @@ -// ApplicationAction.cs: A bar action that starts an application. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -using Morphic.Windows.Native.WindowsCom; - -namespace Morphic.Client.Bar.Data.Actions -{ - using Microsoft.Extensions.Logging; - using Microsoft.Win32; - using Morphic.Core; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Input; - using System.Windows.Interop; - using System.Windows.Media; - using System.Windows.Media.Imaging; - - /// - /// Action to start an application. - /// - [JsonTypeName("application")] - public class ApplicationAction : BarAction - { - private string? exeNameValue; - - /// - /// The actual path to the executable. - /// - public string? AppPath { get; set; } - - public override ImageSource? DefaultImageSource - { - get - { - if (this.AppPath is not null) - { - return Imaging.CreateBitmapSourceFromHIcon( - System.Drawing.Icon.ExtractAssociatedIcon(this.AppPath).Handle, - Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); - } - else - { - return null; - } - } - } - - /// - /// Start a default application. This value will be mapped locally - /// - [JsonProperty("default")] - public string? DefaultAppName { get; set; } - - /// - /// Invoke the value in `exe` as-is, via the shell (explorer). Don't resolve the path. - /// - [JsonProperty("shell")] - public bool Shell { get; set; } - - /// - /// This is a windows store app. The value of `exe` is the Application User Model ID of the app. - /// For example, `Microsoft.WindowsCalculator_8wekyb3d8bbwe!App` - /// - [JsonProperty("appx")] - public bool AppX { get; set; } - - /// - /// true to always start a new instance. false to activate an existing instance. - /// - [JsonProperty("newInstance")] - public bool NewInstance { get; set; } - - /// - /// The initial state of the window. - /// - [JsonProperty("windowStyle")] - public ProcessWindowStyle WindowStyle { get; set; } = ProcessWindowStyle.Normal; - - internal struct MorphicExecutablePathInfo - { - public bool IsAppx; - public string Path; - } - // - // NOTE: we should consider returning error details (e.g. "executable not found", "unknown exeId", "win32 error") from this function - private static MorphicResult ConvertExeIdToExecutablePath(string exeId) - { - bool isAppX = false; - string? appPath = null; - - switch (exeId) - { - case "calculator": - { - // option #1: calc.exe in system folder - //appPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "calc.exe"); - // - // option #2: find calc.exe in paths - appPath = ApplicationAction.SearchPathEnv("calc.exe"); - } - break; - case "firefox": - { - appPath = ApplicationAction.SearchAppPaths("firefox.exe"); - } - break; - case "googleChrome": - { - appPath = ApplicationAction.SearchAppPaths("chrome.exe"); - } - break; - case "microsoftAccess": - { - appPath = ApplicationAction.SearchAppPaths("MSACCESS.EXE"); - } - break; - case "microsoftExcel": - { - appPath = ApplicationAction.SearchAppPaths("excel.exe"); - } - break; - case "microsoftEdge": - { - appPath = ApplicationAction.SearchAppPaths("msedge.exe"); - } - break; - case "microsoftOneNote": - { - appPath = ApplicationAction.SearchAppPaths("OneNote.exe"); - } - break; - case "microsoftOutlook": - { - appPath = ApplicationAction.SearchAppPaths("OUTLOOK.EXE"); - } - break; - case "microsoftPowerPoint": - { - appPath = ApplicationAction.SearchAppPaths("powerpnt.exe"); - } - break; - case "microsoftQuickAssist": - { - // option #1: exactly as written in Quick Assist shortcut (as of 18-Apr-2021): %WINDIR%\system32\quickassist.exe - //var appPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"system32\quickassist.exe"); - // - // option #2: quickassist.exe in system folder - appPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "quickassist.exe"); - // - // option #3: ms url shortcut (NOTE: we have intentionally not used this in case Quick Assist is removed from the system...as the URL does not give us a way to detect that scenario via "file does not exist" checks) - //var appPath = "ms-quick-assist:"; - } - break; - case "microsoftSkype": - { - appPath = ApplicationAction.SearchAppPaths("Skype.exe"); - } - break; - case "microsoftTeams": - { - // NOTE: this is a very odd path, and it's not in the "AppPaths"; if we can find another way to determine the proper launch path or launch programatically, we should consider another method - var userAppDataLocalPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var fixedAppPath = Path.Combine(new string[] { userAppDataLocalPath, "Microsoft", "Teams", "current", "Teams.exe" }); - - // NOTE: we could also probably launch Teams via the "ms-teams:" URI, but that wouldn't necessarily help us detect if it's installed - - // NOTE: in the future, it would be better to launch Update.exe and have it then start Teams (as Microsoft does for their shortcut) -- but we'd probably want to - // detect the Teams.exe file itself to detect if it was installed - //var fixedAppPath = Path.Combine(new string[] { userAppDataLocalPath, "Microsoft", "Teams", "Update.exe" }); - //var params = new string[] { "--processStart", "Teams.exe" }; - - // NOTE: in Windows 11 (and perhaps in recent releases of Windows 10), Microsoft Teams is installed as an Appx package with the name: - // MicrosoftTeams_8wekyb3d8bbwe - // ...so we might refer to this as appx:MicrosoftTeams_8wekyb3d8bbwe!MicrosoftTeams - // NOTE: this path, including "!MicrosoftTeams" at the end, is the Application User Model ID (AUMID) of Microsoft Teams - - if (File.Exists(fixedAppPath) == true) - { - // if the file exists, set appPath to the fixed path - appPath = fixedAppPath; - } - else - { - // if Teams was not installed as an EXE, check to see if it's installed as an APPX package - var packageFamilyName = "MicrosoftTeams_8wekyb3d8bbwe"; - var isPackageInstalledResult = Appx.IsPackageInstalled(packageFamilyName); - if (isPackageInstalledResult.IsError == true) { - return MorphicResult.ErrorResult(); - } - var isPackageInstalled = isPackageInstalledResult.Value!; - - if (isPackageInstalled == true) - { - appPath = packageFamilyName + "!MicrosoftTeams"; - isAppX = true; - } - } - } - break; - case "microsoftWord": - { - appPath = ApplicationAction.SearchAppPaths("Winword.exe"); - } - break; - case "opera": - { - appPath = ApplicationAction.SearchAppPaths("opera.exe"); - } - break; - default: - { - appPath = null; - } - break; - } - - if (appPath is not null) - { - var result = new MorphicExecutablePathInfo() - { - IsAppx = isAppX, - Path = appPath - }; - return MorphicResult.OkResult(result); - } - else - { - return MorphicResult.ErrorResult(); - } - } - - /// - /// Executable name, or the full path to it. If also providing arguments, surround the executable path with quotes. - /// - [JsonProperty("exe")] - public string? ExeName - { - get => this.exeNameValue ?? null; - set - { - this.exeNameValue = value; - this.AppPath = null; // until we find the executable id'd by exeNameValue, AppPath should be null; it will be set to the actual executable path (or AppX identity) - - // OBSERVATION: we currently do not check if a package is installed; we simply assume that it is installed (i.e. available) if the EXE name beings with "appx:" - // NOTE: the "exename" for an appx is "appx:" followed by the AUMID (which includes the package name, plus additional information after the package name) - if (value is not null && value.StartsWith("appx:", StringComparison.InvariantCultureIgnoreCase)) - { - this.AppX = true; - this.AppPath = value.Substring(5); - } - - if (value is not null && value.Length > 0) - { - var convertExeToPathResult = ApplicationAction.ConvertExeIdToExecutablePath(value); - if (convertExeToPathResult.IsError == false) - { - this.AppX = convertExeToPathResult.Value!.IsAppx; - this.AppPath = convertExeToPathResult.Value!.Path; - App.Current.Logger.LogDebug($"Resolved exe file '{this.exeNameValue}' to '{this.AppPath ?? "(null)"}'"); - } - } - - this.IsAvailable = this.AppPath is not null; - } - } - - /// - /// Array of arguments. - /// - [JsonProperty("args")] - public List Arguments { get; set; } = new List(); - - /// - /// The arguments, if they're passed after the exe name. - /// - public string? ArgumentsString { get; set; } - - /// - /// Environment variables to set - /// - [JsonProperty("env")] - public Dictionary EnvironmentVariables { get; set; } = new Dictionary(); - - /// - /// Searches the directories in the PATH environment variable. - /// - /// - /// null if not found. - private static string? SearchPathEnv(string file) - { - // OBSERVATION: the noted alternative may search some standard system directories outside of the PATH if the path env var isn't explicitly provided as an array of strings - // Alternative: https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-pathfindonpathw - return Environment.GetEnvironmentVariable("PATH")? - .Split(Path.PathSeparator) - .Select(p => Path.Combine(p, file)) - .FirstOrDefault(File.Exists); - } - - /// - /// Searches SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths (in both HKCU and HKLM) for an executable. - /// - /// - /// null if not found. - private static string? SearchAppPaths(string file) - { - string? fullPath = null; - - // Look in *\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths (giving priority to the current user, in case an entry exists in both locations) - foreach (RegistryKey rootKey in new[] {Registry.CurrentUser, Registry.LocalMachine}) - { - RegistryKey? key = - rootKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{file}"); - if (key is not null) - { - // capture the default key (which should be the full path with executable) - fullPath = key.GetValue(null) as string; - if (fullPath is not null) - { - break; - } - } - } - - return fullPath; - } - - // NOTE: we should not use GetAppPathForProgId until we have dealt with removing command-line arguments (e.g. "C:\Program Files\Microsoft Office\Root\Office16\WINWORD.EXE /Automation") - private static MorphicResult GetAppPathForProgId(string progId) - { - var getClassIdResult = ApplicationAction.GetClassIdForProgId(progId); - if (getClassIdResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var classId = getClassIdResult.Value!; - - var getPathResult = ApplicationAction.GetLocalServer32PathForClassId(classId); - if (getPathResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var path = getPathResult.Value!; - - // if the command is enclosed by quotes, strip out the actual executable name - if (path.Length > 0 && path.Substring(0, 1) == "\"") - { - var indexOfClosingQuote = path.IndexOf('\"', 1); - if (indexOfClosingQuote > 0) - { - path = path.Substring(1, indexOfClosingQuote - 1); - } - } - - // NOTE: we should check the actual path and remove any command-line arguments (in case the path includes the executable name and also command-line arguments) - // [this could be tricky] - - return MorphicResult.OkResult(path); - } - - // NOTE: this function should return the path to run an executable, given its classId (which can be retrieved from FindClassIdForProgId) - private static MorphicResult GetLocalServer32PathForClassId(string classId) - { - string? result = null; - - bool is64Bit; - switch (IntPtr.Size) - { - case 4: - is64Bit = false; - break; - case 8: - is64Bit = true; - break; - default: - Debug.Assert(false, "OS is not 32-bit or 64-bit"); - return MorphicResult.ErrorResult(); - } - - RegistryKey? key; - if (is64Bit == true) - { - key = Registry.ClassesRoot.OpenSubKey($@"Wow6432Node\CLSID\{classId}\LocalServer32"); - } - else - { - key = Registry.ClassesRoot.OpenSubKey($@"CLSID\{classId}\LocalServer32"); - } - - if (key is not null) - { - var localServer32 = key.GetValue(null) as string; - if (localServer32 is not null) - { - result = localServer32; - } - } - - if (result is not null) - { - return MorphicResult.OkResult(result); - } - else - { - // if we could not find the key or if its default value was null or invalid, return an error - return MorphicResult.ErrorResult(); - } - } - - private static MorphicResult GetClassIdForProgId(string progId) - { - RegistryKey? key = Registry.ClassesRoot.OpenSubKey($@"{progId}\CLSID"); - if (key is not null) - { - var classId = key.GetValue(null) as string; - if (classId is not null) - { - return MorphicResult.OkResult(classId); - } - } - - // if we could not find the key or if its default value was null, return an error - return MorphicResult.ErrorResult(); - } - - private static MorphicResult StripExecutableFromCommand(string command) - { - // we need to split off the executable name from any arguments; it is either enclosed in quotes or it's everything before a space - // - // option 1: enclosed in quotes - // NOTE: there may be other ways of addressing this (such as looking for a file extension on the executable file name) - var indexOfFirstDoubleQuote = command.IndexOf('\"'); - if (indexOfFirstDoubleQuote == 0) - { - var indexOfSecondDoubleQuote = command.IndexOf('\"', indexOfFirstDoubleQuote + 1); - if (indexOfSecondDoubleQuote > 1) - { - command = command.Substring(indexOfFirstDoubleQuote + 1, indexOfSecondDoubleQuote - indexOfFirstDoubleQuote - 1); - return MorphicResult.OkResult(command); - } - else - { - return MorphicResult.ErrorResult(); - } - } - // - // option 2: everything before a space (or everything, if there are no spaces) - var indexOfFirstSpace = command.IndexOf(' '); - if (indexOfFirstSpace > 0) - { - command = command.Substring(0, indexOfFirstSpace); - return MorphicResult.OkResult(command); - } - else - { - return MorphicResult.OkResult(command); - } - } - - private static MorphicResult GetOpenCommandForProgIdClass(string progId) - { - // look up the browser progId's actual executable path (e.g. path to Edge, instead of "MSEdgeHtm") - var browserOpenCommandRegistryKey = Registry.ClassesRoot.OpenSubKey(progId + @"\shell\open\command"); - if (browserOpenCommandRegistryKey is not null) - { - // get the string to launch the browser (e.g. the default registry key value); this result may include arguments - var browserOpenCommand = browserOpenCommandRegistryKey.GetValue(null) as string; - if (browserOpenCommand is not null) - { - var stripExecutableFromCommandResult = ApplicationAction.StripExecutableFromCommand(browserOpenCommand); - if (stripExecutableFromCommandResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - browserOpenCommand = stripExecutableFromCommandResult.Value!; - - return MorphicResult.OkResult(browserOpenCommand); - } - } - - // if we could not get the open command, return failure - return MorphicResult.ErrorResult(); - } - - private static MorphicResult GetPathToExecutableForUrlAssociation(string urlAssociation) - { - var userSelectedBrowserRegistryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\" + urlAssociation + @"\UserChoice", false); - if (userSelectedBrowserRegistryKey is not null) - { - var progId = userSelectedBrowserRegistryKey.GetValue("ProgId") as string; - if (progId is not null) - { - var getOpenCommandForProgIdClassResult = ApplicationAction.GetOpenCommandForProgIdClass(progId); - if (getOpenCommandForProgIdClassResult.IsError == false) - { - var browserOpenCommand = getOpenCommandForProgIdClassResult.Value!; - return MorphicResult.OkResult(browserOpenCommand); - } - } - } - - // if we could not get the open command, return failure - return MorphicResult.ErrorResult(); - } - - private static MorphicResult GetPathToExecutableForFileExtension(string fileExtension) - { - var userSelectedBrowserRegistryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" + fileExtension + @"\UserChoice", false); - if (userSelectedBrowserRegistryKey is not null) - { - var progId = userSelectedBrowserRegistryKey.GetValue("ProgId") as string; - if (progId is not null) - { - var getOpenCommandForProgIdClassResult = ApplicationAction.GetOpenCommandForProgIdClass(progId); - if (getOpenCommandForProgIdClassResult.IsError == false) - { - var browserOpenCommand = getOpenCommandForProgIdClassResult.Value!; - return MorphicResult.OkResult(browserOpenCommand); - } - } - } - - // if we could not get the open command, return failure - return MorphicResult.ErrorResult(); - } - - private static MorphicResult GetDefaultBrowserPath() - { - var userSelectedBrowserRegistryKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", false); - if (userSelectedBrowserRegistryKey is not null) - { - var browserProgId = userSelectedBrowserRegistryKey.GetValue("ProgId") as string; - if (browserProgId is not null) - { - var getOpenCommandForProgIdClassResult = ApplicationAction.GetOpenCommandForProgIdClass(browserProgId); - if (getOpenCommandForProgIdClassResult.IsError == false) - { - var browserOpenCommand = getOpenCommandForProgIdClassResult.Value!; - return MorphicResult.OkResult(browserOpenCommand); - } - } - } - - // if we could not get the browser using the UserChoice registry key, try looking up the file association for an ".htm" file instead - - - - - // if no path could be found, return failure - return MorphicResult.ErrorResult(); - } - - protected override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - if (this.DefaultAppName is not null) - { - // use the default application for this type - switch (this.DefaultAppName!) - { - case "browser": - { - string? associatedExecutablePath = null; - - // try to get the executable for https:// urls - var getAssociatedExecutableForHttpUrlsResult = ApplicationAction.GetPathToExecutableForUrlAssociation("https"); - if (getAssociatedExecutableForHttpUrlsResult.IsError == false) - { - associatedExecutablePath = getAssociatedExecutableForHttpUrlsResult.Value!; - } - - // if we haven't found the default browser yet, look for the default application to open ".htm" files - if (associatedExecutablePath is null) - { - var getAssociatedExecutableForHtmFilesResult = ApplicationAction.GetPathToExecutableForFileExtension(".htm"); - if (getAssociatedExecutableForHtmFilesResult.IsError == false) - { - associatedExecutablePath = getAssociatedExecutableForHtmFilesResult.Value!; - } - } - - // if we still haven't found the default browser, gracefully degrade by trying to use the launch process executable shortcut "https:" instead - if (associatedExecutablePath is null) - { - associatedExecutablePath = "https:"; - } - - var launchBrowserProcessResult = ApplicationAction.LaunchProcess(associatedExecutablePath, new List(), new Dictionary(), this.WindowStyle); - return Task.FromResult(launchBrowserProcessResult); - } - case "email": - { - var launchTarget = "mailto:"; - - var launchMailProcessResult = ApplicationAction.LaunchProcess(launchTarget, new List(), new Dictionary(), this.WindowStyle, true /* useShellExecute should be true for all protocols (e.g. 'mailto:') */); - return Task.FromResult(launchMailProcessResult); - } - default: - { - // unknown - Debug.Assert(false, "Unknown 'default' application type: " + this.DefaultAppName!); - // - MorphicResult result = MorphicResult.ErrorResult(); - return Task.FromResult(result); - } - } - } - - // if we reach here, we need to launch the executable related to this "exe" ID - if (string.IsNullOrEmpty(this.ExeName) || string.IsNullOrEmpty(this.AppPath)) - { - // if we don't have an exeName ID tag, we have failed - // - MorphicResult result = MorphicResult.ErrorResult(); - return Task.FromResult(result); - } - - if (this.AppX) - { - var pid = Appx.Start(this.AppPath); - // - MorphicResult result = pid > 0 ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - return Task.FromResult(result); - } - - // for all other processes, launch the executable - // pathToExecutable - var pathToExecutable = this.AppPath; - // - // useShellExecute - var useShellExecute = true; // default - if (this.Shell) - { - useShellExecute = true; - } - // arguments - List arguments = new List(); - if (this.Arguments.Count > 0) - { - foreach (string argument in this.Arguments) - { - var resolvedString = this.ResolveString(argument, source); - if (resolvedString is not null) - { - arguments.Add(resolvedString); - } - } - } - else - { - var resolvedString = this.ResolveString(this.ArgumentsString, source); - if (resolvedString is not null) - { - arguments.Add(resolvedString); - } - } - // - // environmentVariables - Dictionary environmentVariables = new Dictionary(); - foreach (var (key, value) in this.EnvironmentVariables) - { - var resolvedString = this.ResolveString(value, source); - if (resolvedString is not null) - { - environmentVariables.Add(key, resolvedString); - } - else - { - Debug.Assert(false, "Could not resolve environment variable: key = " + key + ", value = '" + value + "'"); - } - } - // - // windowStyle - var windowStyle = this.WindowStyle; - - var launchProcessResult = ApplicationAction.LaunchProcess(pathToExecutable, arguments, environmentVariables, windowStyle, useShellExecute); - return Task.FromResult(launchProcessResult); - } - - private static MorphicResult LaunchProcess(string pathToExecutable, List arguments, Dictionary environmentVariables, ProcessWindowStyle windowStyle, bool useShellExecute = true) - { - ProcessStartInfo startInfo = new ProcessStartInfo() - { - FileName = pathToExecutable, - ErrorDialog = true, - // This is required to start taskmgr (the UAC prompt) - UseShellExecute = useShellExecute, - WindowStyle = windowStyle - }; - - foreach (string argument in arguments) - { - startInfo.ArgumentList.Add(argument); - } - - foreach (var (key, value) in environmentVariables) - { - startInfo.EnvironmentVariables.Add(key, value); - } - - Process? process = Process.Start(startInfo); - if (process is not null) - { - return MorphicResult.OkResult(); - } - else - { - return MorphicResult.ErrorResult(); - } - } - - /// - /// Activates a running instance of the application. - /// - /// false if it could not be done. - /// - private MorphicResult ActivateInstance() - { - bool success = false; - string? friendlyName = Path.GetFileNameWithoutExtension(this.AppPath); - if (!string.IsNullOrEmpty(friendlyName)) - { - success = Process.GetProcessesByName(friendlyName) - .Where(p => p.MainWindowHandle != IntPtr.Zero) - .OrderByDescending(p => p.StartTime) - .Any(process => WinApi.ActivateWindow(process.MainWindowHandle)); - } - - return success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - } -} diff --git a/Morphic.Client/Bar/Data/Actions/BarAction.cs b/Morphic.Client/Bar/Data/Actions/BarAction.cs deleted file mode 100644 index fef26ec1..00000000 --- a/Morphic.Client/Bar/Data/Actions/BarAction.cs +++ /dev/null @@ -1,412 +0,0 @@ -// BarAction.cs: Actions performed by bar items. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data.Actions -{ - using CountlySDK; - using Microsoft.Extensions.Logging; - using Morphic.Core; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Net.WebSockets; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; - using System.Windows.Media; - - /// - /// An action for a bar item. - /// - [JsonObject(MemberSerialization.OptIn)] - [JsonConverter(typeof(TypedJsonConverter), "kind", "shellExec")] - public abstract class BarAction - { - [JsonProperty("identifier")] - public string Id { get; set; } = string.Empty; - - /// - /// Called by Invoke to perform the implementation-specific action invocation. - /// - /// Button ID, for multi-button bar items. - /// New state, if the button is a toggle. - /// - protected abstract Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null); - - /// - /// Invokes the action. - /// - /// Button ID, for multi-button bar items. - /// New state, if the button is a toggle. - /// - public async Task> InvokeAsync(string? source = null, bool? toggleState = null) - { - MorphicResult result; - try - { - try - { - result = await this.InvokeAsyncImpl(source, toggleState); - } - catch (Exception e) when (!(e is ActionException || e is OutOfMemoryException)) - { - throw new ActionException(e.Message, e); - } - } - catch (ActionException e) - { - App.Current.Logger.LogError(e, $"Error while invoking action for bar {this.Id} {this}"); - - if (e.UserMessage is not null) - { - MessageBox.Show($"There was a problem performing the action:\n\n{e.UserMessage}", - "Custom MorphicBar", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); - } - - result = MorphicResult.ErrorResult(); - } - finally - { - // record telemetry data for this action - await this.SendTelemetryForBarAction(source, toggleState); - } - - return result; - } - - // NOTE: we should refactor this functionality to functions attached to each button (similar to how action callbacks are invoked) - private async Task SendTelemetryForBarAction(string? source = null, bool? toggleState = null) - { - // handle actions which must be filted by id - switch (this.Id) - { - case "log-off": - await App.Current.Countly_RecordEventAsync("SignOut"); - break; - case "volume": - { - if (source == "up") - { - await App.Current.Countly_RecordEventAsync("volumeUp"); - } - else if (source == "down") - { - await App.Current.Countly_RecordEventAsync("volumeDown"); - } - } - break; - case "magnify": - { - if (source == "on") - { - await App.Current.Countly_RecordEventAsync("magnifierShow"); - } - else if (source == "off") - { - await App.Current.Countly_RecordEventAsync("magnifierHide"); - } - } - break; - case "read-aloud": - { - if (source == "play") - { - await App.Current.Countly_RecordEventAsync("readSelectedPlay"); - } - else if (source == "stop") - { - await App.Current.Countly_RecordEventAsync("readSelectedStop"); - } - } - break; - case "color-vision": - switch (source) - { - case "on": - await App.Current.Countly_RecordEventAsync("colorFiltersOn"); - break; - case "off": - await App.Current.Countly_RecordEventAsync("colorFiltersOff"); - break; - } - break; - case "dark-mode": - switch (source) - { - case "on": - await App.Current.Countly_RecordEventAsync("darkModeOn"); - break; - case "off": - await App.Current.Countly_RecordEventAsync("darkModeOff"); - break; - } - break; - case "high-contrast": - switch (source) - { - case "100": - await App.Current.Countly_RecordEventAsync("highContrastOn"); - break; - case "1": - await App.Current.Countly_RecordEventAsync("highContrastOff"); - break; - } - break; - case "night-mode": - switch (source) - { - case "on": - await App.Current.Countly_RecordEventAsync("nightModeOn"); - break; - case "off": - await App.Current.Countly_RecordEventAsync("nightModeOff"); - break; - } - break; - case "": - switch (source) - { - case "com.microsoft.windows.colorFilters/enabled": - { - if (toggleState == true) - { - await App.Current.Countly_RecordEventAsync("colorFiltersOn"); - } - else - { - await App.Current.Countly_RecordEventAsync("colorFiltersOff"); - } - } - break; - case "com.microsoft.windows.highContrast/enabled": - { - if (toggleState == true) - { - await App.Current.Countly_RecordEventAsync("highContrastOn"); - } - else - { - await App.Current.Countly_RecordEventAsync("highContrastOff"); - } - } - break; - case "com.microsoft.windows.nightMode/enabled": - { - if (toggleState == true) - { - await App.Current.Countly_RecordEventAsync("nightModeOn"); - } - else - { - await App.Current.Countly_RecordEventAsync("nightModeOff"); - } - } - break; - case "copy": - // NOTE: this is the basic bar variant of "copy" (below) - { - await App.Current.Countly_RecordEventAsync("screenSnip"); - } - break; - case "dark-mode": - { - if (toggleState == true) - { - await App.Current.Countly_RecordEventAsync("darkModeOn"); - } - else - { - await App.Current.Countly_RecordEventAsync("darkModeOff"); - } - } - break; - case "openallusb": - await App.Current.Countly_RecordEventAsync("openUsbDrives"); - break; - case "ejectallusb": - await App.Current.Countly_RecordEventAsync("ejectUsbDrives"); - break; - case null: - // no tags; this is the Morphie button or another custom element with no known tags - break; - default: - // we do not understand this action type (for telemetry logging purposes) - Debug.Assert(false, "Unknown Action ID (missing telemetry hooks)"); - break; - } - break; - case "screen-zoom": - // this action type's telemetry is logged elsewhere - break; - case "snip": - // NOTE: this is the custom bar variant of "copy" (above) - { - await App.Current.Countly_RecordEventAsync("screenSnip"); - } - break; - default: - // we do not understand this action type (for telemetry logging purposes) - Debug.Assert(false, "Unknown Action ID (missing telemetry hooks)"); - break; - } - } - - /// - /// Resolves "{identifiers}" in a string with its value. - /// - /// - /// - /// null if arg is null - protected string? ResolveString(string? arg, string? source) - { - // Today, there is only "{button}". - return arg?.Replace("{button}", source ?? string.Empty); - } - - public virtual Uri? DefaultImageUri { get; } - public virtual ImageSource? DefaultImageSource { get; } - public virtual bool IsAvailable { get; protected set; } = true; - - public virtual void Deserialized(BarData barData) - { - } - } - - [JsonTypeName("null")] - public class NoOpAction : BarAction - { - protected override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - MorphicResult result = MorphicResult.OkResult(); - return Task.FromResult(result); - } - } - - [JsonTypeName("internal")] - public class InternalAction : BarAction - { - [JsonProperty("function", Required = Required.Always)] - public string? FunctionName { get; set; } - - [JsonProperty("function_on_right_click_also", Required = Required.AllowNull)] - public bool? FunctionOnRightClickAlso { get; set; } - - [JsonProperty("args")] - public Dictionary Arguments { get; set; } = new Dictionary(); - - public string? TelemetryEventName { get; set; } - - protected async override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - try - { - if (this.FunctionName is null) - { - return MorphicResult.OkResult(); - } - - Dictionary resolvedArgs = this.Arguments - .ToDictionary(kv => kv.Key, kv => this.ResolveString(kv.Value, source) ?? string.Empty); - - resolvedArgs.Add("state", toggleState == true ? "on" : "off"); - - return await InternalFunctions.Default.InvokeFunctionAsync(this.FunctionName, resolvedArgs); - } - finally - { - if (this.TelemetryEventName is not null) - { - await App.Current.Countly_RecordEventAsync(this.TelemetryEventName!); - } - } - } - } - - [JsonTypeName("gpii")] - public class GpiiAction : BarAction - { - [JsonProperty("data", Required = Required.Always)] - public JObject RequestObject { get; set; } = null!; - - protected override async Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - ClientWebSocket socket = new ClientWebSocket(); - CancellationTokenSource cancel = new CancellationTokenSource(); - await socket.ConnectAsync(new Uri("ws://localhost:8081/pspChannel"), cancel.Token); - - string requestString = this.RequestObject.ToString(); - byte[] bytes = Encoding.UTF8.GetBytes(requestString); - - ArraySegment sendBuffer = new ArraySegment(bytes); - await socket.SendAsync(sendBuffer, WebSocketMessageType.Text, true, cancel.Token); - - return MorphicResult.OkResult(); - } - } - - [JsonTypeName("shellExec")] - public class ShellExecuteAction : BarAction - { - [JsonProperty("run")] - public string? ShellCommand { get; set; } - - protected override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - bool success = true; - if (!string.IsNullOrEmpty(this.ShellCommand)) - { - Process? process = Process.Start(new ProcessStartInfo() - { - FileName = this.ResolveString(this.ShellCommand, source), - UseShellExecute = true - }); - success = process is not null; - } - - MorphicResult result = success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - return Task.FromResult(result); - } - - public override void Deserialized(BarData barData) - { - } - } - - /// - /// Exception that gets thrown by action invokers. - /// - public class ActionException : ApplicationException - { - /// - /// The message displayed to the user. null to not display a message. - /// - public string? UserMessage { get; set; } - - public ActionException(string? userMessage) - : this(userMessage, userMessage, null) - { - } - public ActionException(string? userMessage, Exception innerException) - : this(userMessage, userMessage, innerException) - { - } - - public ActionException(string? userMessage, string? internalMessage = null, Exception? innerException = null) - : base(internalMessage ?? userMessage ?? innerException?.Message, innerException) - { - this.UserMessage = userMessage; - } - } - -} \ No newline at end of file diff --git a/Morphic.Client/Bar/Data/Actions/Functions.cs b/Morphic.Client/Bar/Data/Actions/Functions.cs deleted file mode 100644 index 389e8efe..00000000 --- a/Morphic.Client/Bar/Data/Actions/Functions.cs +++ /dev/null @@ -1,1028 +0,0 @@ -namespace Morphic.Client.Bar.Data.Actions -{ - using Microsoft.Extensions.Logging; - using Morphic.Core; - using Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Runtime.InteropServices; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Automation; - using System.Windows.Automation.Text; - using UI; - using Windows.Native.Input; - using Windows.Native.Speech; - - [HasInternalFunctions] - // ReSharper disable once UnusedType.Global - accessed via reflection. - public class Functions - { - private readonly static SemaphoreSlim s_captureTextSemaphore = new SemaphoreSlim(1, 1); - - [InternalFunction("snip")] - public static async Task> ScreenSnipAsync(FunctionArgs args) - { - // Hide all application windows - Dictionary opacity = new Dictionary(); - HashSet visible = new HashSet(); - try - { - foreach (Window window in App.Current.Windows) - { - if (window is BarWindow || window is QuickHelpWindow) - { - if (window.AllowsTransparency) - { - opacity[window] = window.Opacity; - window.Opacity = 0; - } - else - { - visible.Add(window); - window.Visibility = Visibility.Collapsed; - } - } - } - - // Give enough time for the windows to disappear - await Task.Delay(500); - - //// method 1: hold down the windows key while pressing shift + s - //// NOTE: this method does not seem to work when we have uiAccess set to true in our manifest (oddly) - //const uint windowsKey = 0x5b; // VK_LWIN - //Keyboard.PressKey(windowsKey, true); - //System.Windows.Forms.SendKeys.SendWait("+s"); - //Keyboard.PressKey(windowsKey, false); - - // method 2: open up the special windows URI of ms-screenclip: - var openPath = "ms-screenclip:"; - Process.Start(new ProcessStartInfo(openPath) - { - UseShellExecute = true - }); - } - finally - { - // Give enough time for snip tool to grab the screen without the morphic UI. - await Task.Delay(3000); - - // Restore the windows - foreach ((Window window, double o) in opacity) - { - window.Opacity = o; - } - - foreach (Window window in visible) - { - window.Visibility = Visibility.Visible; - } - } - - return MorphicResult.OkResult(); - } - - [InternalFunction("menu", "key=Morphic")] - public async static Task> ShowMenuAsync(FunctionArgs args) - { - // NOTE: this internal function is only called by the MorphicBar's Morphie menu button - await App.Current.ShowMenuAsync(null, Morphic.Client.Menu.MorphicMenu.MenuOpenedSource.morphicBarIcon); - return MorphicResult.OkResult(); - } - - [InternalFunction("volumeUp")] - public static async Task> VolumeUpAsync(FunctionArgs args) - { - args.Arguments.Add("direction", "up"); - args.Arguments.Add("amount", "6"); - return await SetVolumeAsync(args); - } - - [InternalFunction("volumeDown")] - public static async Task> VolumeDownAsync(FunctionArgs args) - { - args.Arguments.Add("direction", "down"); - args.Arguments.Add("amount", "6"); - return await SetVolumeAsync(args); - } - - internal static MorphicResult GetMuteState() - { - try - { - var audioEndpoint = Windows.Native.Audio.AudioEndpoint.GetDefaultAudioOutputEndpoint(); - - // if we didn't get a state in the request, try to reverse the state - var state = audioEndpoint.GetMasterMuteState(); - - return MorphicResult.OkResult(state); - } - catch - { - return MorphicResult.ErrorResult(); - } - } - - [InternalFunction("volumeMute")] - public static async Task> VolumeMuteAsync(FunctionArgs args) - { - bool newState; - if (args.Arguments.Keys.Contains("state")) - { - newState = (args["state"] == "on"); - } - else - { - var getMuteStateResult = Functions.GetMuteState(); - if (getMuteStateResult.IsSuccess == true) - { - newState = getMuteStateResult.Value!; - } - else - { - // if we cannot get the current value, gracefully degrade (i.e. assume that the volume is not muted) - newState = false; - } - } - - try - { - // set the mute state to the new state value - var audioEndpoint = Windows.Native.Audio.AudioEndpoint.GetDefaultAudioOutputEndpoint(); - audioEndpoint.SetMasterMuteState(newState); - } - catch - { - return MorphicResult.ErrorResult(); - } - - return MorphicResult.OkResult(); - } - - /// - /// Lowers or raises the volume. - /// - /// direction: "up"/"down", amount: number of 1/100 to move - /// - [InternalFunction("volume", "direction", "amount=6")] - public static async Task> SetVolumeAsync(FunctionArgs args) - { - // NOTE: ideally we should switch this functionality to use AudioEndpoint.SetMasterVolumeLevel instead - - IntPtr taskTray = WinApi.FindWindow("Shell_TrayWnd", IntPtr.Zero); - if (taskTray != IntPtr.Zero) - { - int action = args["direction"] == "up" - ? WinApi.APPCOMMAND_VOLUME_UP - : WinApi.APPCOMMAND_VOLUME_DOWN; - - // Each command moves the volume by 2 notches. - int times = Math.Clamp(Convert.ToInt32(args["amount"]), 1, 20) / 2; - for (int n = 0; n < times; n++) - { - WinApi.SendMessage(taskTray, WinApi.WM_APPCOMMAND, IntPtr.Zero, - (IntPtr)WinApi.MakeLong(0, (short)action)); - } - } - - return MorphicResult.OkResult(); - } - - private static async Task> ClearClipboardAsync(uint numberOfRetries, TimeSpan interval) - { - // NOTE from Microsoft documentation (something to think about when working on this in the future...and perhaps something we need to handle): - /* "The Clipboard class can only be used in threads set to single thread apartment (STA) mode. - * To use this class, ensure that your Main method is marked with the STAThreadAttribute attribute." - * https://docs.microsoft.com/es-es/dotnet/api/system.windows.forms.clipboard.clear?view=net-5.0 - */ - for (var i = 0; i < numberOfRetries; i++) - { - try - { - // NOTE: some developers have reported unhandled exceptions with this function call, even when inside a try...catch block. If we experience that, we may need to look at our threading model, UWP alternatives, and Win32 API alternatives. - Clipboard.Clear(); - return MorphicResult.OkResult(); - } - catch - { - // failed to copy to clipboard; wait an interval and then try again - await Task.Delay(interval); - } - } - - App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); - return MorphicResult.ErrorResult(); - } - - /// - /// Reads the selected text. - /// - /// action: "play", "pause", or "stop" - /// - [InternalFunction("readAloud", "action")] - public static async Task> ReadAloudAsync(FunctionArgs args) - { - string action = args["action"]; - switch (action) - { - case "pause": - App.Current.Logger.LogError("ReadAloud: pause not supported."); - - return MorphicResult.ErrorResult(); - - case "stop": - App.Current.Logger.LogDebug("ReadAloud: Stop reading selected text."); - TextToSpeechHelper.Instance.Stop(); - - return MorphicResult.OkResult(); - - case "play": - string? selectedText = null; - - try - { - App.Current.Logger.LogDebug("ReadAloud: Getting selected text."); - - // activate the target window (i.e. topmost/last-active window, rather than the MorphicBar); we will then capture the current selection in that window - // NOTE: ideally we would activate the last window as part of our atomic operation, but we really have no control over whether or not another application - // or the user changes the activated window (and our internal code is also not set up to block us from moving activation/focus temporarily). - await SelectionReader.Default.ActivateLastActiveWindow(); - - // as a primary strategy, try using the built-in Windows functionality for capturing the current selection via UI automation - // NOTE: this does not work with some apps (such as Internet Explorer...but also others) - bool captureTextViaAutomationSucceeded = false; - // - TextPatternRange[]? textRangeCollection = null; - // - // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block - await s_captureTextSemaphore.WaitAsync(); - // - try - { - var focusedElement = AutomationElement.FocusedElement; - if (focusedElement is not null) - { - object? pattern = null; - if (focusedElement.TryGetCurrentPattern(TextPattern.Pattern, out pattern)) - { - if ((pattern is not null) && (pattern is TextPattern textPattern)) - { - // App.Current.Logger.LogDebug("ReadAloud: Capturing select text range(s)."); - - // get the collection of text ranges in the selection; note that this can be a disjoint collection if multiple disjoint items were selected - textRangeCollection = textPattern.GetSelection(); - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Selected element is not text."); - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: No element is currently selected."); - } - } - finally - { - s_captureTextSemaphore.Release(); - } - // - // if we just captured a text range collection (i.e. were able to copy the current selection), convert that capture into a string now - StringBuilder? selectedTextBuilder = null; - if (textRangeCollection is not null) - { - // we have captured a range (presumably either an empty or non-empty selection) - selectedTextBuilder = new StringBuilder(); - - // append each text range - foreach (var textRange in textRangeCollection) - { - if (textRange is not null) - { - selectedTextBuilder.Append(textRange.GetText(-1 /* maximumRange */)); - } - } - - //if (selectedTextBuilder is not null /* && stringBuilder.Length > 0 */) - //{ - selectedText = selectedTextBuilder.ToString(); - captureTextViaAutomationSucceeded = true; - - if (selectedText != String.Empty) - { - App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); - } - //} - } - - // as a backup strategy, use the clipboard and send ctrl+c to the target window to capture the text contents (while preserving as much of the previous - // clipboard's contents as possible); this is necessary in Internet Explorer and some other programs - if (captureTextViaAutomationSucceeded == false) - { - // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block - await s_captureTextSemaphore.WaitAsync(); - // - try - { - // App.Current.Logger.LogDebug("ReadAloud: Attempting to back up current clipboard."); - - Dictionary clipboardContentsToRestore = new Dictionary(); - - var previousClipboardData = Clipboard.GetDataObject(); - if (previousClipboardData is not null) - { - // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to capture format(s) of contents."); - string[]? previousClipboardFormats = previousClipboardData.GetFormats(); - if (previousClipboardFormats is not null) - { - // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to back up current clipboard."); - - foreach (var format in previousClipboardFormats) - { - object? dataObject; - try - { - dataObject = previousClipboardData.GetData(format, false /* autoConvert */); - } - catch - { - // NOTE: in the future, we should look at using Project Reunion to use the UWP APIs (if they can deal with this scenario better) - // see: https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.clipboard?view=winrt-19041 - // see: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance - App.Current.Logger.LogDebug("ReadAloud: Unable to back up clipboard contents; this can happen with files copied to the clipboard, etc."); - - return MorphicResult.ErrorResult(); - } - clipboardContentsToRestore[format] = dataObject; - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents, but we were unable to obtain their formats."); - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Current clipboard has no contents."); - } - - // clear the current clipboard - App.Current.Logger.LogDebug("ReadAloud: Clearing the current clipboard."); - try - { - // try to clear the clipboard for up to 500ms (4 delays of 125ms) - await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125)); - } - catch - { - App.Current.Logger.LogDebug("ReadAloud: Could not clear the current clipboard."); - } - - // copy the current selection to the clipboard - App.Current.Logger.LogDebug("ReadAloud: Sending Ctrl+C to copy the current selection to the clipboard."); - await SelectionReader.Default.GetSelectedTextAsync(System.Windows.Forms.SendKeys.SendWait); - - // wait 100ms (an arbitrary amount of time, but in our testing some wait is necessary...even with the WM-triggered copy logic above) - // NOTE: perhaps, in the future, we should only do this if our first call to Clipboard.GetText() returns (null? or) an empty string; - // or perhaps we should wait up to a certain number of milliseconds to receive a SECOND WM (the one that GetSelectedTextAsync - // waited for). - await Task.Delay(100); - - // capture the current selection - var selectionWasCopiedToClipboard = false; - var textCopiedToClipboard = Clipboard.GetText(); - if (textCopiedToClipboard is not null) - { - selectionWasCopiedToClipboard = true; - - // we now have our selected text - selectedText = textCopiedToClipboard; - - if (selectedText is not null) - { - App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); - } - } - else - { - var copiedDataFormats = Clipboard.GetDataObject()?.GetFormats(); - if (copiedDataFormats is not null) - { - selectionWasCopiedToClipboard = true; - - // var formatsCsvBuilder = new StringBuilder(); - // formatsCsvBuilder.Append("["); - // if (copiedDataFormats.Length > 0) - // { - // formatsCsvBuilder.Append("\""); - // formatsCsvBuilder.Append(String.Join("\", \"", copiedDataFormats)); - // formatsCsvBuilder.Append("\""); - // } - // formatsCsvBuilder.Append("]"); - - // App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy text; instead it copied data in these format(s): " + formatsCsvBuilder.ToString()); - App.Current.Logger.LogDebug("ReadAloud: Ctrl+C copied non-text (un-speakable) contents to the clipboard."); - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy anything to the clipboard."); - } - } - - // restore the previous clipboard's contents - // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore the previous clipboard's contents"); - // - if (selectionWasCopiedToClipboard == true) - { - // App.Current.Logger.LogDebug("ReadAloud: Clearing the selected text from the clipboard."); - try - { - // try to clear the clipboard for up to 500ms (4 delays of 125ms) - await Functions.ClearClipboardAsync(5, new TimeSpan(0,0,0,0,125)); - } - catch - { - App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); - } - } - // - if (clipboardContentsToRestore.Count > 0) - { - // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore " + clipboardContentsToRestore.Count.ToString() + " item(s) to the clipboard."); - } - else - { - // App.Current.Logger.LogDebug("ReadAloud: there is nothing to restore to the clipboard."); - } - // - foreach (var (format, data) in clipboardContentsToRestore) - { - // NOTE: sometimes, data is null (which is not something that SetData can accept) so we have to just skip that element - if (data is not null) - { - Clipboard.SetData(format, data); - } - } - // - App.Current.Logger.LogDebug("ReadAloud: Clipboard restoration complete"); - } - finally - { - s_captureTextSemaphore.Release(); - } - } - } - catch (Exception ex) - { - App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text."); - - return MorphicResult.ErrorResult(); - } - - if (selectedText is not null) - { - if (selectedText != String.Empty) - { - try - { - App.Current.Logger.LogDebug("ReadAloud: Saying selected text."); - - var sayResult = await TextToSpeechHelper.Instance.Say(selectedText); - if (sayResult.IsError == true) - { - App.Current.Logger.LogError("ReadAloud: Error saying selected text."); - - return MorphicResult.ErrorResult(); - } - - return MorphicResult.OkResult(); - } - catch (Exception ex) - { - App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text."); - - return MorphicResult.ErrorResult(); - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: No text to say; skipping 'say' command."); - - return MorphicResult.OkResult(); - } - } else { - // could not capture any text - // App.Current.Logger.LogError("ReadAloud: Could not capture any selected text; this may or may not be an error."); - - return MorphicResult.ErrorResult(); - } - default: - throw new Exception("invalid code path"); - } - } - - /// - /// Sends key strokes to the active application. - /// - /// keys: the keys (see MSDN for SendKeys.Send()) - /// - [InternalFunction("sendKeys", "keys")] - public static async Task> SendKeysAsync(FunctionArgs args) - { - await SelectionReader.Default.ActivateLastActiveWindow(); - System.Windows.Forms.SendKeys.SendWait(args["keys"]); - return MorphicResult.OkResult(); - } - - [InternalFunction("signOut")] - public static async Task> SignOutAsync(FunctionArgs args) - { - var success = Morphic.Windows.Native.WindowsSession.WindowsSession.LogOff(); - return success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - - [InternalFunction("openAllUsbDrives")] - public static async Task> OpenAllUsbDrivesAsync(FunctionArgs args) - { - App.Current.Logger.LogError("OpenAllUsbDrives"); - - var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync(); - if (getRemovableDisksAndDrivesResult.IsError == true) - { - Debug.Assert(false, "Could not get list of removable drives"); - App.Current.Logger.LogError("Could not get list of removable drives"); - return MorphicResult.ErrorResult(); - } - var removableDrives = getRemovableDisksAndDrivesResult.Value!.RemovableDrives; - - // as we only want to open usb drives which are mounted (i.e. not USB drives which have had their "media" ejected but who still have drive letters assigned)... - var mountedRemovableDrives = new List(); - foreach (var drive in removableDrives) - { - var getIsMountedResult = await drive.GetIsMountedAsync(); - if (getIsMountedResult.IsError == true) - { - Debug.Assert(false, "Could not determine if drive is mounted"); - App.Current.Logger.LogError("Could not determine if drive is mounted"); - // gracefully degrade; skip this disk - continue; - } - var driveIsMounted = getIsMountedResult.Value!; - - if (driveIsMounted) - { - mountedRemovableDrives.Add(drive); - } - } - - // now open all the *mounted* removable disks - foreach (var drive in mountedRemovableDrives) - { - // get the drive's root path (e.g. "E:\"); note that we intentionally get the root path WITH the backslash so that we don't launch autoplay, etc. - var tryGetDriveRootPathResult = await drive.TryGetDriveRootPathAsync(); - if (tryGetDriveRootPathResult.IsError == true) - { - Debug.Assert(false, "Could not get removable drive's root path"); - App.Current.Logger.LogError("Could not get removable drive's root path"); - // gracefully degrade; skip this disk - continue; - } - var driveRootPath = tryGetDriveRootPathResult.Value!; - - // NOTE: there is also an API call which may be able to do this more directly - // see: https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems - - // NOTE: we might also consider getting the current process for Explorer.exe and then asking it to "explore" the drive - - App.Current.Logger.LogError("Opening USB drive"); - - Process.Start(new ProcessStartInfo() - { - FileName = driveRootPath, - UseShellExecute = true - }); - } - - return MorphicResult.OkResult(); - } - - [InternalFunction("ejectAllUsbDrives")] - public static async Task> EjectAllUsbDrivesAsync(FunctionArgs args) - { - App.Current.Logger.LogError("EjectAllUsbDrives"); - - var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync(); - if (getRemovableDisksAndDrivesResult.IsError == true) - { - Debug.Assert(false, "Could not get list of removable disks"); - App.Current.Logger.LogError("Could not get list of removable disks"); - return MorphicResult.ErrorResult(); - } - var removableDisks = getRemovableDisksAndDrivesResult.Value!.RemovableDisks; - - // now eject all the removable disks - var allDisksRemoved = true; - foreach (var disk in removableDisks) - { - App.Current.Logger.LogError("Safely ejecting drive"); - - // NOTE: "safe eject" in this circumstance means to safely eject the usb device (removing it from the PnP system, not physically ejecting media) - var safeEjectResult = disk.SafelyRemoveDevice(); - if (safeEjectResult.IsError == true) - { - allDisksRemoved = false; - } - - // wait 50ms between ejection - await Task.Delay(50); - } - - if (allDisksRemoved == false) - { - return MorphicResult.ErrorResult(); - } - - return allDisksRemoved ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - - private struct GetRemovableDisksAndDrivesResult - { - public List AllDisks; - public List RemovableDisks; // physical volumes - public List RemovableDrives; // logical volumes (media / partition); these can have drive letters - } - // - private static async Task> GetRemovableDisksAndDrivesAsync() - { - // get a list of all disks (but not non-disks such as CD-ROM drives) - var getAllDisksResult = await Morphic.Windows.Native.Devices.Disk.GetAllDisksAsync(); - if (getAllDisksResult.IsError == true) - { - Debug.Assert(false, "Cannot get list of disks"); - return MorphicResult.ErrorResult(); - } - - // filter out all disks which are not removable - var allDisks = getAllDisksResult.Value!; - var removableDisks = new List(); - foreach (var disk in allDisks) - { - var getIsRemovableResult = disk.GetIsRemovable(); - if (getIsRemovableResult.IsError == true) - { - Debug.Assert(false, "Cannot determine if disk is removable"); - return MorphicResult.ErrorResult(); - } - var diskIsRemovable = getIsRemovableResult.Value!; - if (diskIsRemovable) - { - removableDisks.Add(disk); - } - } - - // now get all the drives associated with our removable disks - var removableDrives = new List(); - foreach (var removableDisk in removableDisks) - { - var getDrivesForDiskResult = await removableDisk.GetDrivesAsync(); - if (getDrivesForDiskResult.IsError == true) - { - Debug.Assert(false, "Cannot get list of drives for removable disk"); - // gracefully degrade; skip this disk - continue; - } - var drivesForRemovableDisk = getDrivesForDiskResult.Value!; - - removableDrives.AddRange(drivesForRemovableDisk); - } - - var result = new GetRemovableDisksAndDrivesResult - { - AllDisks = allDisks, - RemovableDisks = removableDisks, - RemovableDrives = removableDrives - }; - - return MorphicResult.OkResult(result); - } - - internal async static Task> GetDarkModeStateAsync() - { - var osVersion = Morphic.Windows.Native.OsVersion.OsVersion.GetWindowsVersion(); - if (osVersion == Windows.Native.OsVersion.WindowsVersion.Win10_v1809) - { - // Windows 10 v1809 - - // NOTE: this is hard-coded, as a patch, because the solutions registry does not yet understand how to capture/apply settings across incompatible handlers - // [and trying to call the Windows 10 v1903+ handlers for apps/system "light theme" will result in a memory access exception under v1809] - // [also: only "AppsUseLightTheme" (and not "SystemUsesLightTheme") existed properly under Windows 10 v1809] - - var openPersonalizeKeyResult = Morphic.Windows.Native.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize", true); - if (openPersonalizeKeyResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var personalizeKey = openPersonalizeKeyResult.Value!; - - // get the current setting - bool appsUseLightThemeAsBool; - var getAppsUseLightThemeResult = personalizeKey.GetValue("AppsUseLightTheme"); - if (getAppsUseLightThemeResult.IsError == true) - { - if (getAppsUseLightThemeResult.Error == Windows.Native.Registry.RegistryKey.RegistryValueError.ValueDoesNotExist) - { - // default AppsUseLightTheme (inverse of dark mode state) on Windows 10 v1809 is true - appsUseLightThemeAsBool = true; - } - else - { - return MorphicResult.ErrorResult(); - } - } - else - { - var appsUseLightThemeAsUInt32 = getAppsUseLightThemeResult.Value!; - appsUseLightThemeAsBool = (appsUseLightThemeAsUInt32 != 0) ? true : false; - } - - // dark theme state is the inverse of AppsUseLightTheme - var darkThemeState = !appsUseLightThemeAsBool; - - return MorphicResult.OkResult(darkThemeState); - } - else if (osVersion is null) - { - // error - return MorphicResult.ErrorResult(); - } - else - { - // Windows 10 v1903+ - - // get system dark/light theme - Setting systemThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeSystem); - var getSystemThemeValueResult = await systemThemeSetting.GetValueAsync(); - if (getSystemThemeValueResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var lightThemeSystemAsObject = getSystemThemeValueResult.Value!; - var lightThemeSystemAsBool = (bool)lightThemeSystemAsObject; - - // set apps dark/light theme - Setting appsThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeApps); - var getAppsThemeValueResult = await appsThemeSetting.GetValueAsync(); - if (getAppsThemeValueResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var lightThemeAppsAsObject = getAppsThemeValueResult.Value!; - var lightThemeAppsAsBool = (bool)lightThemeSystemAsObject; - - // if either apps or system theme is set to "not light", then return true - var darkModeIsEnabled = ((lightThemeSystemAsBool == false) || (lightThemeAppsAsBool == false)); - return MorphicResult.OkResult(darkModeIsEnabled); - } - } - - internal async static Task> SetDarkModeStateAsync(bool state) - { - var osVersion = Morphic.Windows.Native.OsVersion.OsVersion.GetWindowsVersion(); - if (osVersion == Windows.Native.OsVersion.WindowsVersion.Win10_v1809) - { - // Windows 10 v1809 - - // NOTE: this is hard-coded, as a patch, because the solutions registry does not yet understand how to capture/apply settings across incompatible handlers - // [and trying to call the Windows 10 v1903+ handlers for apps/system "light theme" will result in a memory access exception under v1809] - // [also: only "AppsUseLightTheme" (and not "SystemUsesLightTheme") existed properly under Windows 10 v1809] - - var openPersonalizeKeyResult = Morphic.Windows.Native.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize", true); - if (openPersonalizeKeyResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var personalizeKey = openPersonalizeKeyResult.Value!; - - // set apps dark/light theme - // - uint newAppsUseLightThemeAsUInt32 = state ? (uint)0 : (uint)1; // NOTE: these are inverted (because we are setting "light state" using the inverse of the "dark state" parameter - // - // set the setting to the inverted state - var setAppsUseLightThemeResult = personalizeKey.SetValue("AppsUseLightTheme", newAppsUseLightThemeAsUInt32); - if (setAppsUseLightThemeResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - // see: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-wininichange - var pointerToImmersiveColorSetString = Marshal.StringToHGlobalUni("ImmersiveColorSet"); - try - { - // notify all windows that we have changed a setting in the "win ini" settings - _ = PInvoke.User32.SendMessage(PInvoke.User32.HWND_BROADCAST, PInvoke.User32.WindowMessage.WM_WININICHANGE, IntPtr.Zero, pointerToImmersiveColorSetString); - } - finally - { - Marshal.FreeHGlobal(pointerToImmersiveColorSetString); - } - } - else if (osVersion is null) - { - // error - return MorphicResult.ErrorResult(); - } - else - { - // Windows 10 v1903+ - - /* - * NOTE: in addition to the SPI implementation (in code, below), we could also turn on/off the dark theme (via powershell...or possibly via direct registry access); here are the corresponding PowerShell commands - * NOTE: we use registry access to get/set dark mode under Windows 10 <=v1809 (see code above); the "system dark theme" was introduced in Windows 10 v1903 - * - * SWITCH TO LIGHT MODE: - * New-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize -Name SystemUsesLightTheme -Value 1 -Type Dword -Force - * New-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize -Name AppsUseLightTheme -Value 1 -Type Dword -Force - * - * SWITCH TO DARK MODE: - * New-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize -Name SystemUsesLightTheme -Value 0 -Type Dword -Force - * New-ItemProperty -Path HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize -Name AppsUseLightTheme -Value 0 -Type Dword -Force - */ - - // set system dark/light theme - Setting systemThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeSystem); - await systemThemeSetting.SetValueAsync(!state); - - // set apps dark/light theme - Setting appsThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeApps); - await appsThemeSetting.SetValueAsync(!state); - } - - return MorphicResult.OkResult(); - } - - [InternalFunction("darkMode")] - public static async Task> DarkModeAsync(FunctionArgs args) - { - // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state" - bool on; - if (args.Arguments.Keys.Contains("value")) - { - on = (args["value"] == "on"); - } - else if (args.Arguments.Keys.Contains("state")) - { - on = (args["state"] == "on"); - } - else - { - System.Diagnostics.Debug.Assert(false, "Function 'darkMode' did not receive a new state"); - on = false; - } - - var setDarkModeStateResult = await Functions.SetDarkModeStateAsync(on); - if (setDarkModeStateResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - return MorphicResult.OkResult(); - } - - // - - const string WORD_RUNNING_MESSAGE = "You need to exit Word in order to use the Word Simplify buttons.\n\n(1) Quit Word.\n(2) Use the Word Simplify buttons to add or remove the simplified ribbon(s) you want.\n(3) Re-launch Word."; - - private static bool IsSafeToModifyRibbonFile_WarnUser() - { - // make sure Word is not running before attempting to change the word ribbon enable/disable state - var isWordRunningResult = Morphic.Integrations.Office.WordRibbon.IsWordRunning(); - if (isWordRunningResult.IsError == true) - { - // NOTE: realistically, we might not want to create a modal message box during an async function. - MessageBox.Show("Sorry, we cannot detect if Word is running.\n\nThis feature is currently unavailable."); - } - var wordIsRunning = isWordRunningResult.Value!; - // - if (wordIsRunning == true) - { - MessageBox.Show(Functions.WORD_RUNNING_MESSAGE); - return false; - } - - // if Word is not running, it's safe to proceed - return true; - } - - [InternalFunction("basicWordRibbon")] - public static async Task> ToggleBasicWordRibbonAsync(FunctionArgs args) - { - // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state" - bool on; - if (args.Arguments.Keys.Contains("value")) - { - on = (args["value"] == "on"); - } - else if (args.Arguments.Keys.Contains("state")) - { - on = (args["state"] == "on"); - } - else - { - System.Diagnostics.Debug.Assert(false, "Function 'basicWordRibbon' did not receive a new state"); - on = false; - } - - if (Functions.IsSafeToModifyRibbonFile_WarnUser() == false) - { - // Word is running, so we are choosing not to execute this function - return MorphicResult.ErrorResult(); - } - - if (on == true) - { - var enableRibbonResult = Morphic.Integrations.Office.WordRibbon.EnableBasicSimplifyRibbon(); - return enableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - else - { - var disableRibbonResult = Morphic.Integrations.Office.WordRibbon.DisableBasicSimplifyRibbon(); - return disableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - } - - [InternalFunction("essentialsWordRibbon")] - public static async Task> ToggleEssentialsWordRibbonAsync(FunctionArgs args) - { - // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state" - bool on; - if (args.Arguments.Keys.Contains("value")) - { - on = (args["value"] == "on"); - } - else if (args.Arguments.Keys.Contains("state")) - { - on = (args["state"] == "on"); - } - else - { - System.Diagnostics.Debug.Assert(false, "Function 'essentialsWordRibbon' did not receive a new state"); - on = false; - } - - if (Functions.IsSafeToModifyRibbonFile_WarnUser() == false) - { - // Word is running, so we are choosing not to execute this function - return MorphicResult.ErrorResult(); - } - - if (on == true) - { - var enableRibbonResult = Morphic.Integrations.Office.WordRibbon.EnableEssentialsSimplifyRibbon(); - return enableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - else - { - var disableRibbonResult = Morphic.Integrations.Office.WordRibbon.DisableEssentialsSimplifyRibbon(); - return disableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - } - } - - // - - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Windows API naming")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Windows API naming")] - private static class WinApi - { - //public const int APPCOMMAND_VOLUME_MUTE = 8; - public const int APPCOMMAND_VOLUME_DOWN = 9; - public const int APPCOMMAND_VOLUME_UP = 10; - public const int WM_APPCOMMAND = 0x319; - - [DllImport("user32.dll")] - public static extern IntPtr FindWindow(string lpClassName, IntPtr lpWindowName); - - [DllImport("user32.dll")] - public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - public static int MakeLong(short low, short high) - { - return (low & 0xffff) | ((high & 0xffff) << 16); - } - } - } -} diff --git a/Morphic.Client/Bar/Data/Actions/InternalFunctions.cs b/Morphic.Client/Bar/Data/Actions/InternalFunctions.cs deleted file mode 100644 index 2f38824c..00000000 --- a/Morphic.Client/Bar/Data/Actions/InternalFunctions.cs +++ /dev/null @@ -1,190 +0,0 @@ -// InternalFunctions.cs: Handles the internal functions for bar items. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data.Actions -{ - using Microsoft.Extensions.Logging; - using Morphic.Core; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using System.Threading.Tasks; - - /// - /// Handles the invocation of internal functions, used by the InternalAction class. - /// - /// The functions are public static methods decorated with [InternalFunction("fname")], in any class in this - /// assembly (which also has the HasInternalFunctions attribute). - /// - public class InternalFunctions - { - /// Default singleton instance. - public static InternalFunctions Default = new InternalFunctions(); - - /// All internal functions. - private readonly Dictionary all; - - public delegate Task> InternalFunction(FunctionArgs args); - - protected InternalFunctions() - { - this.all = InternalFunctions.FindAllFunctions() - .ToDictionary(attr => attr.FunctionName.ToLowerInvariant(), attr => attr); - } - - /// - /// Gets the methods that handle the built-in functions. - /// - /// - private static IEnumerable FindAllFunctions() - { - // Get all public static methods in all public classes in this assembly, which both have the InternalFunction - // attribute - IEnumerable methods = typeof(InternalFunctions).Assembly.GetTypes() - .Where(t => t.IsClass && t.IsPublic && t.GetCustomAttributes().Any()) - .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static)); - - // Add the methods decorated with [InternalFunction] - foreach (MethodInfo method in methods) - { - InternalFunctionAttribute? attr = method.GetCustomAttribute(); - if (attr is not null) - { - try - { - attr.SetFunction((InternalFunction)method.CreateDelegate(typeof(InternalFunction))); - } - catch - { - System.Diagnostics.Debug.Assert(false, "Could not wire up delegate to internal function"); - } - yield return attr; - } - } - } - - /// - /// Invokes a built-in function. - /// - /// The function name. - /// The parameters. - /// - public Task> InvokeFunctionAsync(string functionName, Dictionary functionArgs) - { - App.Current.Logger.LogDebug($"Invoking built-in function '{functionName}'"); - - Task> result; - - if (this.all.TryGetValue(functionName.ToLowerInvariant(), - out InternalFunctionAttribute? functionAttribute)) - { - FunctionArgs args = new FunctionArgs(functionAttribute, functionArgs); - result = functionAttribute.Function(args); - } - else - { - throw new ActionException($"No internal function found for '{functionName}'"); - } - - return result; - } - } - - /// - /// Marks a method (or a class containing such methods) that's a built-in function for bar actions. - /// - [AttributeUsage(AttributeTargets.Method)] - public class InternalFunctionAttribute : Attribute - { - public string FunctionName { get; } - public string[] RequiredArguments { get; } - public InternalFunctions.InternalFunction Function { get; private set; } = null!; - - /// - /// Defines an internal function for the bar. - /// - /// Name of the function.. - /// - /// Name of each required argument, if any. For optional parameters, use "name=default". - /// - public InternalFunctionAttribute(string functionName, params string[] requiredArgs) - { - this.RequiredArguments = requiredArgs; - this.FunctionName = functionName; - } - - public void SetFunction(InternalFunctions.InternalFunction internalFunction) - { - this.Function = internalFunction; - } - - /// - /// Checks a given arguments dictionary for require values, and adding the value for those that are missing. - /// - /// The arguments (gets modified). - /// - public void CheckRequiredArguments(Dictionary arguments) - { - foreach (string required in this.RequiredArguments) - { - string[] split = required.Split('=', 2); - string name = split[0]; - - if (!arguments.ContainsKey(name)) - { - string? defaultValue = split.Length > 1 ? split[1] : null; - if (defaultValue is null) - { - throw new ActionException( - $"Internal function {this.FunctionName} invoked without parameter {name}"); - } - - arguments.Add(name, defaultValue); - } - } - } - } - - /// - /// Identifies a class having internal functions. - /// - [AttributeUsage(AttributeTargets.Class)] - public class HasInternalFunctionsAttribute : Attribute - { - } - - public class FunctionArgs - { - public string FunctionName { get; } - public Dictionary Arguments { get; } - - /// - /// Gets an argument value by its name, or an empty string if there's no such argument. - /// - /// - public string this[string argumentName] => this.Arguments.TryGetValue(argumentName, out string? value) - ? value - : string.Empty; - - /// - /// Creates arguments for a function. - /// - /// The function attribute of the method that handles the internal function. - /// The arguments. - public FunctionArgs(InternalFunctionAttribute functionAttribute, Dictionary args) - { - this.FunctionName = functionAttribute.FunctionName; - this.Arguments = args.ToDictionary(kv => kv.Key, kv => kv.Value); - - functionAttribute.CheckRequiredArguments(this.Arguments); - } - } -} diff --git a/Morphic.Client/Bar/Data/Actions/SettingAction.cs b/Morphic.Client/Bar/Data/Actions/SettingAction.cs deleted file mode 100644 index 4d6d9d39..00000000 --- a/Morphic.Client/Bar/Data/Actions/SettingAction.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace Morphic.Client.Bar.Data.Actions -{ - using Microsoft.Extensions.DependencyInjection; - using Morphic.Core; - using Newtonsoft.Json; - using Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - using System.Threading.Tasks; - - [JsonTypeName("setting")] - public class SettingAction : BarAction - { - [JsonProperty("settingId", Required = Required.Always)] - public string SettingId { get; set; } = string.Empty; - - public Setting? Setting { get; private set; } - public Solutions Solutions { get; private set; } = null!; - - protected override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - Setting? setting; - - if (this.Setting is null && !string.IsNullOrEmpty(source)) - { - setting = this.Solutions.GetSetting(source); - // OBSERVATION: we do not await on this call; we may want to do so - setting.SetValueAsync(toggleState); - } - else - { - setting = this.Setting; - } - - if (setting is null) - { - MorphicResult okResult = MorphicResult.OkResult(); - return Task.FromResult(okResult); - } - - switch (source) - { - case "inc": - return setting.Increment(1); - case "dec": - return setting.Increment(-1); - case "on": - return setting.SetValueAsync(true); - case "off": - return setting.SetValueAsync(false); - } - - MorphicResult errorResult = MorphicResult.ErrorResult(); - return Task.FromResult(errorResult); - } - - public async Task CanExecute(string id) - { - bool canExecute = true; - - if (Setting?.Range is not null) - { - var range = Setting.Range; - var idRequiresCountRefresh = Setting.Id == "zoom"; - - var min = await range.GetMin(0, idRequiresCountRefresh); - var max = await range.GetMax(0, idRequiresCountRefresh) - 1; - - var currentValue = (int)(Setting.CurrentValue ?? 0); - - if (id == "inc" && currentValue >= max) - { - canExecute = false; - } - else if (id == "dec" && currentValue <= min) - { - canExecute = false; - } - } - - return canExecute; - } - - public override void Deserialized(BarData bar) - { - base.Deserialized(bar); - - this.Solutions = bar.ServiceProvider.GetRequiredService(); - if (!string.IsNullOrEmpty(this.SettingId)) - { - this.Setting = this.Solutions.GetSetting(this.SettingId); - } - } - } -} diff --git a/Morphic.Client/Bar/Data/Actions/WebAction.cs b/Morphic.Client/Bar/Data/Actions/WebAction.cs deleted file mode 100644 index 16993c0f..00000000 --- a/Morphic.Client/Bar/Data/Actions/WebAction.cs +++ /dev/null @@ -1,93 +0,0 @@ -// WebAction.cs: Bar action that opens a website. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data.Actions -{ - using Microsoft.Extensions.Logging; - using Morphic.Core; - using Newtonsoft.Json; - using System; - using System.Diagnostics; - using System.Threading.Tasks; - - /// - /// A web-link action. - /// - [JsonTypeName("link")] - public class WebAction : BarAction - { - private string? urlString; - - [JsonProperty("url", Required = Required.Always)] - public string UrlString - { - get => this.Uri?.ToString() ?? this.urlString ?? string.Empty; - set - { - if (Uri.TryCreate(value, UriKind.Absolute, out Uri? uri)) - { - // validate our uri - switch (uri?.Scheme.ToLowerInvariant()) { - case "http": - case "https": - // allowed - break; - case "skype": - // allowed for now, but in the future we may want to launch Skype directly and handle this information seperately - break; - default: - // all other schemes (as well as a null scheme) are disallowed - uri = null; - break; - } - - // save our validated uri - this.Uri = uri; - } - else - { - this.urlString = value; - App.Current.Logger.LogWarning($"Unable to parse url '{this.urlString}'"); - } - } - } - - public Uri? Uri { get; set; } - - /// - /// Use the site's favicon as the default. - /// - public override Uri? DefaultImageUri - { - get - { - return null; -// this.Uri is not null ? new Uri($"https://icons.duckduckgo.com/ip2/{this.Uri.Host}.ico") : null; - } - } - - protected override Task> InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - bool success = true; - if (this.Uri is not null) - { - Process? process = Process.Start(new ProcessStartInfo() - { - FileName = this.ResolveString(this.Uri?.ToString(), source), - UseShellExecute = true - }); - success = process is not null; - } - - MorphicResult result = success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - return Task.FromResult(result); - } - } -} diff --git a/Morphic.Client/Bar/Data/BarButton.cs b/Morphic.Client/Bar/Data/BarButton.cs deleted file mode 100644 index 3b800f86..00000000 --- a/Morphic.Client/Bar/Data/BarButton.cs +++ /dev/null @@ -1,268 +0,0 @@ -// BarButton.cs: Button widget on the bar -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.IO; - using System.Net; - using System.Runtime.CompilerServices; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Controls; - using System.Windows.Media; - using System.Xml; - using Config; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using UI.BarControls; - - /// - /// Button bar item. - /// - [JsonTypeName("button")] - [BarControl(typeof(ButtonBarControl))] - public class BarButton : BarItem, INotifyPropertyChanged - { - private string? imagePath; - private string? imageValue; - private ImageSource? imageSource; - private Uri? remoteImage; - - public BarButton(BarData bar) : base(bar) - { - } - - [JsonProperty("configuration.image_path")] - public string? FrontendImagePath { get; set; } - - /// - /// The original image, as defined in json. - /// - [JsonProperty("configuration.image_url")] - public string? ImageValue - { - get => this.imageValue; - set - { - this.imageValue = value ?? string.Empty; - if (string.IsNullOrEmpty(this.imageValue)) - { - this.ImagePath = string.Empty; - } - else - { - Uri.TryCreate(this.imageValue, UriKind.Absolute, out Uri? uri); - string? localPath = null; - if (uri is null || uri.IsFile) - { - localPath = BarImages.GetBarIconFile(this.imageValue); - if (localPath is null) - { - uri = new Uri(this.Bar.FrontEndUri, this.FrontendImagePath); - } - } - - if (localPath is not null) - { - this.ImagePath = localPath; - } - else if (uri is not null) - { - // Download later. - this.RemoteImage = uri; - } - } - } - } - - /// - /// The image to use. - /// - public ImageSource? ImageSource - { - get => this.imageSource; - set - { - this.imageSource = value; - this.OnPropertyChanged(); - } - } - - private Canvas? _xamlContent; - public Canvas? XamlContent - { - get => _xamlContent; - set - { - _xamlContent = value; - this.OnPropertyChanged(); - } - } - - /// - /// The real local path of the item's image. - /// - public string ImagePath - { - get => this.imagePath ?? string.Empty; - private set => this.imagePath = value; - } - - // Limit the concurrent downloads. - private static SemaphoreSlim downloads = new SemaphoreSlim(8); - private static HashSet downloading = new HashSet(); - private static HashSet downloadComplete = new HashSet(); - - /// - /// Loads the image specified by ImagePath. - /// - /// true on success. - public async Task LoadImage() - { - bool success = false; - - // Download the remote image. - if (this.DownloadRequired && this.RemoteImage is not null) - { - using WebClient wc = new WebClient(); - string tempFile = this.ImagePath + ".new"; - try - { - try - { - await downloads.WaitAsync(); - - // Check if the image is being downloaded by another bar item. - bool downloadRequired = downloading.Add(this.ImagePath); - - if (downloadRequired) - { - // Download it - this.Logger.LogDebug("Downloading {remoteImage}", this.RemoteImage); - await wc.DownloadFileTaskAsync(this.RemoteImage, tempFile); - } - else - { - // wait for the other bar's download to complete - while (!downloadComplete.Contains(this.ImagePath)) - { - await Task.Delay(500); - } - } - } - finally - { - downloads.Release(); - } - FileInfo fileInfo = new FileInfo(tempFile); - - if (fileInfo.Exists && fileInfo.Length > 0) - { - File.Move(tempFile, this.ImagePath, true); - } - } - catch (Exception e) when (!(e is OutOfMemoryException)) - { - // Ignore - this.Logger.LogWarning(e, "Download failed {remoteImage}", this.RemoteImage); - } - finally - { - File.Delete(tempFile); - downloadComplete.Add(this.ImagePath); - } - } - - // Load the local image. - if (!string.IsNullOrEmpty(this.ImagePath) && File.Exists(this.ImagePath)) - { - if (Path.GetExtension(imagePath)?.ToLowerInvariant() == ".xaml") - { - var xamlFileStream = new FileStream(this.ImagePath, FileMode.Open, FileAccess.Read); - // TODO: when we move to .NET 5, set useRestrictiveXamlReader to true - var xamlAsCanvas = (Canvas)System.Windows.Markup.XamlReader.Load(new XmlTextReader(xamlFileStream) /*, true */); - this.XamlContent = xamlAsCanvas; - - success = true; - } - else - { - this.ImageSource = BarImages.CreateImageSource(this.ImagePath); - - success = this.ImageValue is not null; - } - } - - // Fallback to a default image. - if (!success) - { - ImageSource? source = this.Action?.DefaultImageSource; - if (source is not null) - { - this.ImageSource = source; - success = true; - } - else - { - Uri? defaultUri = this.Action?.DefaultImageUri; - if (defaultUri is not null && this.RemoteImage != defaultUri) - { - this.RemoteImage = defaultUri; - success = await this.LoadImage(); - } - } - } - - return success; - } - - /// - /// true if downloading a new copy of a remote image is needed. - /// - public bool DownloadRequired { get; set; } - - /// - /// The URL to the remote image. - /// - public Uri? RemoteImage - { - get => this.remoteImage; - private set - { - this.remoteImage = value; - if (this.remoteImage is not null) - { - this.ImagePath = AppPaths.GetCacheFile(this.remoteImage, out bool exists); - this.DownloadRequired = !exists - || (DateTime.Now - File.GetLastWriteTime(this.ImagePath)).TotalDays > 2; - } - } - } - - public override void Deserialized() - { - base.Deserialized(); - - _ = this.LoadImage(); - } - - public bool ShowIcon => true;// string.IsNullOrEmpty(this.IconPath); - - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} diff --git a/Morphic.Client/Bar/Data/BarData.cs b/Morphic.Client/Bar/Data/BarData.cs deleted file mode 100644 index 954ca86c..00000000 --- a/Morphic.Client/Bar/Data/BarData.cs +++ /dev/null @@ -1,520 +0,0 @@ -// BarData.cs: Information about a bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -using Morphic.Service; - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Config; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - - /// - /// Describes a bar. - /// - [JsonObject(MemberSerialization.OptIn)] - public class BarData : IDisposable, IDeserializable - { - private List fileWatchers = new List(); - - public event EventHandler? ReloadRequired; - - public BarData() : this(null) - { - } - - public BarData(IServiceProvider? serviceProvider) - { - this.ServiceProvider = serviceProvider ?? App.Current.ServiceProvider; - SessionOptions sessionOptions = this.ServiceProvider.GetRequiredService(); - this.FrontEndUri = sessionOptions.FrontEndUri; - this.BarEditorWebAppUri = sessionOptions.BarEditorWebAppUri; - } - - public IServiceProvider ServiceProvider { get; set; } - - public Uri FrontEndUri { get; } - - public Uri BarEditorWebAppUri { get; } - - /// - /// Where the bar data was loaded from (a url or path). - /// - public string Source { get; set; } = string.Empty; - - /// - /// Bar identifier (currently unused by the client) - /// - [JsonProperty("id")] - public string? Id { get; set; } - - /// - /// Name of the bar (currently unused by the client) - /// - [JsonProperty("name")] - public string? Name { get; set; } - - /// - /// Title of the bar (the window caption) - /// - [JsonProperty("title")] - public string? Title { get; set; } = "Custom MorphicBar"; - - /// - /// Size of everything. - /// - [JsonProperty("scale")] - public double Scale { get; set; } = 1; - - /// - /// What to do if all buttons do not fit. - /// - [JsonProperty("overflow")] - public BarOverflow Overflow { get; set; } = BarOverflow.Resize; - - /// Initial bar positions. - [JsonProperty("position", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public BarPosition Position { get; set; } = new BarPosition(); - - /// Initial bar positions. - [JsonProperty("secondaryBar", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public SecondaryBar SecondaryBar { get; set; } = new SecondaryBar(); - - /// - /// Base theme for bar items - items will take values from this if they haven't got their own. - /// - [JsonProperty("itemTheme", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public BarItemTheme DefaultTheme { get; set; } = new BarItemTheme(); - - /// - /// Base theme for the buttons in the multi-button bar items. - /// - [JsonProperty("controlTheme", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public BarItemTheme ControlTheme { get; set; } = new BarItemTheme(); - - /// - /// Theme for the bar. - /// - [JsonProperty("barTheme", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public Theme BarTheme { get; set; } = new Theme(); - - [JsonProperty("sizes", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public BarSizes Sizes { get; set; } = new BarSizes(); - - /// - /// Gets all items. - /// - [JsonProperty("items")] - public List AllItems { get; set; } = new List(); - - /// - /// Determines if an item should be on the primary bar. - /// - /// The item. - /// true if the item belongs on the primary bar. - private bool IsPrimaryItem(BarItem item) - { - return !item.Hidden && !item.Overflow; - } - - /// - /// Determines if an item should be on the secondary bar. - /// - /// The item. - /// true if the item belongs on the secondary bar. - private bool IsSecondaryItem(BarItem item) - { - return !item.Hidden && !this.IsPrimaryItem(item); - } - - /// - /// Gets the items for the main bar. - /// - public IEnumerable PrimaryItems => this.AllItems.Where(this.IsPrimaryItem) - .OrderByDescending(item => item.Priority); - - // OBSERVATION: the usage of the "Overflow" bool in sorting and the general nomenclature of "Overflow" and "Priority" leave room for bugs; - // at first glance, "Overflow" should be a filter for secondary items rather than a descending sort order and - // "Priority" (which is the index value) seems like it should ordered in ascending order; - // we should rethink our terminology and refactor the logic here and the variable naming and logic related to these variables - // - /// - /// Gets the items for the additional buttons. - /// - public IEnumerable SecondaryItems => this.AllItems.Where(this.IsSecondaryItem) - .OrderByDescending(item => item.Overflow) - .ThenByDescending(item => item.Priority); - - public string? CommunityId { get; set; } - - private ILogger logger = App.Current.ServiceProvider.GetRequiredService>(); - - /// - /// Loads bar data from either a local file, or a url. - /// - /// The service provider/ - /// The local path or remote url. - /// The json content, if already loaded. - /// true to also include the default bar data. - /// The bar data - public static BarData? Load(IServiceProvider serviceProvider, string barSource, string? content = null, bool includeDefault = true) - { - BarData? defaultBar; - if (includeDefault) - { - defaultBar = BarData.Load(serviceProvider, AppPaths.GetConfigFile("default-bar.json5", true), null, false); - // Mark the items as being from the default specification - defaultBar?.AllItems.ForEach(item => item.IsDefault = true); - - // OBSERVATION: we need a better way to determine if this is the basic bar or another bar - var isBasicBar = (barSource == AppPaths.GetConfigFile("basic-bar.json5", true)); - // - if (isBasicBar == true) - { - // if extra bar items were specified in the config file, add them to the left side of the MorphicBar now - var morphicBarExtraItems = ConfigurableFeatures.MorphicBarExtraItems; - if (morphicBarExtraItems.Count > 0) - { - List extraBarItems = new List(); - foreach (var extraItemData in morphicBarExtraItems) - { - BarItem extraBarItem; - var extraBarItemShouldBeAdded = false; - - switch (extraItemData.type) - { - case "link": - { - extraBarItem = new BarButton(defaultBar); - extraBarItem.ToolTipHeader = extraItemData.tooltipHeader; - extraBarItem.ToolTip = extraItemData.tooltipText; - extraBarItem.Text = extraItemData.label ?? ""; - // - extraBarItem.Action = new Morphic.Client.Bar.Data.Actions.WebAction(); - ((Morphic.Client.Bar.Data.Actions.WebAction)extraBarItem.Action!).UrlString = extraItemData.url ?? ""; - extraBarItemShouldBeAdded = true; - } - break; - case "action": - { - extraBarItem = new BarButton(defaultBar); - extraBarItem.ToolTipHeader = extraItemData.tooltipHeader; - extraBarItem.ToolTip = extraItemData.tooltipText; - extraBarItem.Text = extraItemData.label ?? ""; - // - var extraBarItemInternalAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - extraBarItemInternalAction.TelemetryEventName = "morphicBarExtraItem"; - extraBarItem.Action = extraBarItemInternalAction; - ((Morphic.Client.Bar.Data.Actions.InternalAction)extraBarItem.Action!).FunctionName = extraItemData.function!; - extraBarItemShouldBeAdded = true; - } - break; - case "control": - { - extraBarItem = new BarMultiButton(defaultBar); - extraBarItem.ToolTipHeader = extraItemData.tooltipHeader; - extraBarItem.ToolTip = extraItemData.tooltipText; - // - switch (extraItemData.feature) - { - case "usbopeneject": - { - extraBarItem.Text = extraItemData.label ?? "USB Drives (All)"; - // - var openAllUsbAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - openAllUsbAction.TelemetryEventName = "morphicBarExtraItem"; - openAllUsbAction.FunctionName = "openAllUsbDrives"; - var openButton = new BarMultiButton.ButtonInfo - { - Text = "Open", - Action = openAllUsbAction, - TelemetryCategory = "morphicBarExtraItem", - Tooltip = "Open All USB Drives", - Value = "openallusb" - }; - // - var ejectAllUsbAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - ejectAllUsbAction.TelemetryEventName = "morphicBarExtraItem"; - ejectAllUsbAction.FunctionName = "ejectAllUsbDrives"; - var ejectButton = new BarMultiButton.ButtonInfo - { - Text = "Eject", - Action = ejectAllUsbAction, - TelemetryCategory = "morphicBarExtraItem", - Tooltip = "Eject All USB Drives", - Value = "ejectallusb" - }; - // - ((BarMultiButton)extraBarItem).Buttons = new Dictionary - { - { "open", openButton }, - { "eject", ejectButton } - }; - // - extraBarItemShouldBeAdded = true; - } - break; - case "volume": - { - extraBarItem.Text = extraItemData.label ?? "Volume"; - // - var volumeUpAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - volumeUpAction.TelemetryEventName = "volumeUp"; - volumeUpAction.FunctionName = "volumeUp"; - var volumeUpButton = new BarMultiButton.ButtonInfo - { - Text = "+", - Action = volumeUpAction, - TelemetryCategory = "volumeUp", - Tooltip = "Increases the volume|Makes all sounds louder.|Volume cannot go louder", - Value = "volumeUp" - }; - // - var volumeDownAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - volumeDownAction.TelemetryEventName = "volumeDown"; - volumeDownAction.FunctionName = "volumeDown"; - var volumeDownButton = new BarMultiButton.ButtonInfo - { - Text = "-", - Action = volumeDownAction, - TelemetryCategory = "volumeDown", - Tooltip = "Decreases the volume|Makes all sounds quieter.|Volume cannot go quieter", - Value = "volumeDown" - }; - // - var volumeMuteAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - volumeMuteAction.TelemetryEventName = "volumeMute"; - volumeMuteAction.FunctionName = "volumeMute"; - var volumeMuteButton = new BarMultiButton.ButtonInfo - { - Text = "Mute", - Action = volumeMuteAction, - TelemetryCategory = "volumeMute", - Toggle = true, - Tooltip = "Mutes all sounds from your computer|Mutes your speakers - but does NOT mute your microphone.", - Value = "volumeMute" - }; - // - ((BarMultiButton)extraBarItem).Buttons = new Dictionary - { - { "volumeUp", volumeUpButton }, - { "volumeDown", volumeDownButton }, - { "volumeMute", volumeMuteButton } - }; - ((BarMultiButton)extraBarItem).Menu = new Dictionary() - { - { "setting", "sound" }, - { "learn", "volmute" }, - { "demo", "volmute" } - }; - ((BarMultiButton)extraBarItem).AutoSize = true; - // - extraBarItemShouldBeAdded = true; - } - break; - case "wordsimplify": - { - extraBarItem.Text = extraItemData.label ?? "Word Simplify"; - // - var basicWordRibbonAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - basicWordRibbonAction.TelemetryEventName = "morphicBarExtraItem"; // basicWordRibbonToggle - basicWordRibbonAction.FunctionName = "basicWordRibbon"; - var basicWordRibbonButton = new BarMultiButton.ButtonInfo - { - Text = "Basic", - Action = basicWordRibbonAction, - TelemetryCategory = "morphicBarExtraItem", - Toggle = true, - Tooltip = "Adds a new 'Basic Items' ribbon to Word|Gives you a new simpler ribbon with just the basic items on it.", - Value = "basicwordribbon" - }; - // - var essentialsWordRibbonAction = new Morphic.Client.Bar.Data.Actions.InternalAction(); - essentialsWordRibbonAction.TelemetryEventName = "morphicBarExtraItem"; // essentialsWordRibbonToggle - essentialsWordRibbonAction.FunctionName = "essentialsWordRibbon"; - var essentialsWordRibbonButton = new BarMultiButton.ButtonInfo - { - Text = "Essentials", - Action = essentialsWordRibbonAction, - TelemetryCategory = "morphicBarExtraItem", - Toggle = true, - Tooltip = "Adds a new 'Essential Items' ribbon to Word|Gives you a new ribbon with essential items gathered from all other ribbons.", - Value = "essentialswordribbon" - }; - // - ((BarMultiButton)extraBarItem).Buttons = new Dictionary - { - { "basic", basicWordRibbonButton }, - { "essentials", essentialsWordRibbonButton } - }; - ((BarMultiButton)extraBarItem).Menu = new Dictionary() - { - { "learn", "wordsimplify" }, - { "demo", "wordsimplify" } - }; - ((BarMultiButton)extraBarItem).AutoSize = true; - // - // NOTE: we shouldonly show this item if Word is actually installed - extraBarItemShouldBeAdded = Morphic.Integrations.Office.WordRibbon.IsOfficeInstalled(); - } - break; - default: - extraBarItem.Text = extraItemData.label ?? ""; - // NOTE: we don't know what this button is, so do not show it - extraBarItemShouldBeAdded = false; - break; - } - } - break; - default: - // unknown type; this should be an impossible code path - throw new NotImplementedException(); - } - //extraBarItem.ColorValue = "#00FF00"; - // - if (extraBarItemShouldBeAdded == true) - { - defaultBar?.AllItems.Add(extraBarItem); - } - } - - // add a spacer entry - BarButton spacerBarItem = new BarButton(defaultBar); - spacerBarItem.ToolTipHeader = ""; - spacerBarItem.ToolTip = ""; - spacerBarItem.Text = ""; - spacerBarItem.ColorValue = "#FFFFFF"; - // - defaultBar?.AllItems.Add(spacerBarItem); - } - } - } - else - { - defaultBar = null; - } - - App.Current.Logger.LogInformation("Loading bar from {source}", barSource); - - BarData? bar; - - using (TextReader reader = content is null - ? (TextReader)File.OpenText(barSource) - : new StringReader(content)) - { - bar = BarJson.Load(serviceProvider, reader, defaultBar); - } - - bar.Source = barSource; - if (File.Exists(barSource)) - { - bar.AddWatcher(barSource); - } - - return bar; - } - - private bool hasDeserialized; - - /// - /// Called when the bar has been deserialised. This can be called twice, for the default bar and the user's bar. - /// - public void Deserialized() - { - // Make the theme of each item inherit the default theme. - this.BarTheme.Apply(Theme.DefaultBar()); - this.DefaultTheme.Apply(Theme.DefaultItem()); - this.ControlTheme.Apply(Theme.DefaultControl()).Apply(this.DefaultTheme); - - this.AllItems.ForEach(item => - { - item.IsDefault = !this.hasDeserialized; - item.Deserialized(); - }); - - this.hasDeserialized = true; - } - - /// - /// Makes a url from a string containing a url or a local path (absolute or relative). - /// - /// - /// - public static Uri MakeUrl(string input) - { - if (!Uri.TryCreate(input, UriKind.Absolute, out Uri? uri)) - { - // Assume it's a relative path. - string fullPath = Path.GetFullPath(input); - uri = new Uri(fullPath); - } - - return uri; - } - - private void AddWatcher(string file) - { - string fullPath = Path.GetFullPath(file); - string dir = Path.GetDirectoryName(fullPath)!; - string filename = Path.GetFileName(fullPath); - - FileSystemWatcher watcher = new FileSystemWatcher(dir) - { - Filter = filename, - NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.Size - | NotifyFilters.FileName, - EnableRaisingEvents = true - }; - - watcher.Changed += this.WatcherOnChanged; - watcher.Created += this.WatcherOnChanged; - watcher.Renamed += this.WatcherOnChanged; - - this.fileWatchers.Add(watcher); - } - - private CancellationTokenSource? changed; - - private async void WatcherOnChanged(object sender, FileSystemEventArgs e) - { - this.changed?.Cancel(); - this.changed = new CancellationTokenSource(); - - try - { - // Wait for the change events to finish. - await Task.Delay(1000, this.changed.Token); - this.changed = null; - App.Current.Dispatcher.Invoke(() => this.ReloadRequired?.Invoke(this, e)); - } - catch (TaskCanceledException) - { - // Do nothing. - } - } - - public void Dispose() - { - this.fileWatchers.ForEach(fileWatcher => - { - fileWatcher.EnableRaisingEvents = false; - fileWatcher.Dispose(); - }); - this.fileWatchers.Clear(); - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Bar/Data/BarEnums.cs b/Morphic.Client/Bar/Data/BarEnums.cs deleted file mode 100644 index 7efd16f1..00000000 --- a/Morphic.Client/Bar/Data/BarEnums.cs +++ /dev/null @@ -1,50 +0,0 @@ -// BarEnums.cs: Enumerations used by the bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - - -namespace Morphic.Client.Bar.Data -{ - public enum Position - { - Absolute = 0, - Percent = 1, - Left = 2, - Top = 3, - Right = 4, - Bottom = 5, - Center = 6, - Centre = 6, - Middle = 6 - } - - public enum ExpanderRelative - { - Both = 0, - Primary, - Secondary - } - - public enum BarOverflow - { - Resize = 0, - Wrap, - Scale, - Hide, - Secondary - } - - public enum BarItemSize - { - TextOnly = 0, - Small, - Medium, - Large - } -} diff --git a/Morphic.Client/Bar/Data/BarItem.cs b/Morphic.Client/Bar/Data/BarItem.cs deleted file mode 100644 index 87966186..00000000 --- a/Morphic.Client/Bar/Data/BarItem.cs +++ /dev/null @@ -1,236 +0,0 @@ -// BarItem.cs: An item on a bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.Collections.Generic; - using System.Reflection; - using System.Windows.Media; - using Actions; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using UI.BarControls; - - /// - /// A bar item. - /// - /// For items of kind == "action", configuration.identifier is used to lookup an item from actions.js. The object - /// from there is merged onto this, just before deserialisation. - /// - [JsonObject(MemberSerialization.OptIn)] - [JsonConverter(typeof(TypedJsonConverter), "widget", "button")] - public class BarItem - { - public BarItem(BarData bar) - { - this.Bar = bar; - } - - protected ILogger Logger = App.Current.ServiceProvider.GetRequiredService>(); - private string? text; - private string? uiName; - - /// - /// The bar that owns this item. - /// - public BarData Bar { get; set; } - - // NOTE: this property has been deprecated, as the determination of if an item should be on the bar (and not in the overflow drawer) is now based on position and available room - // - ///// - ///// true if the item is to be displayed on the pull-out bar. - ///// - //[JsonProperty("is_primary")] - //public bool IsPrimary { get; set; } - - /// - /// true if the item should over-flow to the secondary bar, because it doesn't fit. - /// - public bool Overflow { get; set; } - - /// - /// true if this item is a built-in item, from the default bar json. - /// - public bool IsDefault { get; set; } - - /// - /// Don't over-flow this item to the secondary bar. - /// - [JsonProperty("no_overflow")] - public bool NoOverflow { get; set; } - - [JsonProperty("configuration", ObjectCreationHandling = ObjectCreationHandling.Replace)] - [JsonConverter(typeof(TypedJsonConverter), "kind", "null")] - public BarAction? Action { get; set; } - - /// - /// The text displayed on the item. - /// - [JsonProperty("configuration.label")] - public string Text - { - get => this.text ?? this.DefaultText ?? string.Empty; - set => this.text = value; - } - - /// - /// The text used by UI automation - this is what narrator reads. - /// - [JsonProperty("configuration.uiName")] - public string UiName - { - get - { - string name = this.uiName ?? this.Text; - return string.IsNullOrEmpty(name) - ? this.ToolTipHeader ?? this.ToolTip ?? string.Empty - : name; - } - set => this.uiName = value; - } - - /// - /// The text displayed on the item, if Text is not set. - /// - [JsonProperty("configuration.defaultLabel")] - public string? DefaultText { get; set; } - - /// - /// Tooltip header text (default is the this.Text). - /// - [JsonProperty("configuration.tooltipHeader")] - public string? ToolTipHeader { get; set; } - - /// - /// Tooltip smaller text. - /// - [JsonProperty("configuration.tooltip")] - public string? ToolTip { get; set; } - - /// - /// The background colour (setter from json to allow empty strings). - /// - [JsonProperty("configuration.color")] - public string ColorValue - { - set - { - if (!string.IsNullOrEmpty(value)) - { - try - { - if (ColorConverter.ConvertFromString(value) is Color color) - { - this.Color = color; - } - } - catch (FormatException) - { - // invalid value - // NOTE: we should log this error - } - } - } - get => ""; - } - - /// - /// The background colour. - /// - public Color Color - { - get => this.Theme.Background ?? Colors.Transparent; - set - { - this.Theme.Background = value; - this.Theme.InferStateThemes(true); - } - } - - /// - /// Don't display this item. - /// - [JsonProperty("hidden")] - public bool Hidden { get; set; } - - /// - /// Theme for the item. - /// - [JsonProperty("theme", DefaultValueHandling = DefaultValueHandling.Populate)] - public BarItemTheme Theme { get; set; } = new BarItemTheme(); - - /// - /// Theme for the control buttons. - /// - [JsonProperty("controlTheme", DefaultValueHandling = DefaultValueHandling.Populate)] - public BarItemTheme ControlTheme { get; set; } = new BarItemTheme(); - - /// - /// Items are sorted by this. - /// - [JsonProperty("priority")] - public int Priority { get; set; } - - [JsonProperty("configuration.size")] - public BarItemSize Size { get; set; } = BarItemSize.Large; - - [JsonProperty("configuration.menu")] - public Dictionary Menu { get; set; } = new Dictionary(); - - [JsonProperty("configuration.telemetryCategory")] - public string? TelemetryCategory { get; set; } - - /// - /// The type of control used. This is specified by using BarControl attribute in a subclass of this. - /// - public Type ControlType => this.GetType().GetCustomAttribute()?.Type!; - - /// - /// Called when the bar has loaded. - /// - public virtual void Deserialized() - { - // Inherit the default theme - this.Theme.Inherit(this.Bar.DefaultTheme); - this.Theme.InferStateThemes(); - this.ControlTheme.Inherit(this.Bar.ControlTheme).Inherit(this.Bar.DefaultTheme); - this.ControlTheme.InferStateThemes(); - - this.Action?.Deserialized(this.Bar); - } - } - - /// - /// Image bar item. - /// - [JsonTypeName("image")] - [BarControl(typeof(ImageBarControl))] - public class BarImage : BarButton - { - public BarImage(BarData bar) : base(bar) - { - } - } - - /// - /// Used by a BarItem subclass to identify the control used to display the item. - /// - public class BarControlAttribute : Attribute - { - public Type Type { get; } - - public BarControlAttribute(Type type) - { - this.Type = type; - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Bar/Data/BarItemTheme.cs b/Morphic.Client/Bar/Data/BarItemTheme.cs deleted file mode 100644 index da98bb02..00000000 --- a/Morphic.Client/Bar/Data/BarItemTheme.cs +++ /dev/null @@ -1,183 +0,0 @@ -// BarItemTheme.cs: Describes the visual appearance of a bar and its items. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System.ComponentModel; - using System.Linq; - using System.Reflection; - using System.Windows.Forms; - using System.Windows.Media; - using Newtonsoft.Json; - - /// - /// Theme for a bar item. - /// - [JsonObject(MemberSerialization.OptIn)] - public class BarItemTheme : Theme - { - /// The theme for when the mouse is over the item. - [JsonProperty("hover", ObjectCreationHandling = ObjectCreationHandling.Replace)] - public Theme Hover { get; set; } = new Theme(); - - /// The theme for when the item has keyboard focus. - [JsonProperty("focus", ObjectCreationHandling = ObjectCreationHandling.Replace)] - public Theme Focus { get; set; } = new Theme(); - - /// The theme for when the item is being clicked (mouse is down). - [JsonProperty("active", ObjectCreationHandling = ObjectCreationHandling.Replace)] - public Theme Active { get; set; } = new Theme(); - - /// The theme for when the item is checked (toggle buttons). - [JsonProperty("checked", ObjectCreationHandling = ObjectCreationHandling.Replace)] - public Theme Checked { get; set; } = new Theme(); - - public BarItemTheme() - { - } - - public BarItemTheme(Theme theme) - { - this.Apply(theme); - } - - public BarItemTheme Inherit(BarItemTheme theme) - { - this.Apply(theme); - this.Hover.Apply(theme.Hover); - this.Focus.Apply(theme.Focus); - this.Active.Apply(theme.Active); - this.Checked.Apply(theme.Checked); - return this; - } - - /// - /// Generate the themes for the different states that are unset, based on the colour. - /// - /// - public void InferStateThemes(bool force = false) - { - if (force || this.Hover.Background is null) - { - this.Hover.Background = this.LightenColor(this.Background ?? Colors.Transparent, 0.25f); - } - - if (force || this.Active.Background is null) - { - this.Active.Background = this.LightenColor(this.Background ?? Colors.Transparent, 0.5f); - } - - if (force || this.Focus.Background is null) - { - this.Focus.Background = this.Hover.Background; - } - } - - private Color LightenColor(Color color, float amount) - { - System.Drawing.Color c = - ControlPaint.Light(System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B), amount); - return Color.FromArgb(c.A, c.R, c.G, c.B); - } - } - - /// - /// Theme for the bar. - /// - [JsonObject(MemberSerialization.OptIn)] - public class BarTheme : Theme - { - - } - - /// - /// A theme. - /// - [JsonObject(MemberSerialization.OptIn)] - public class Theme : INotifyPropertyChanged - { - /// Text colour. - [JsonProperty("color")] - public Color? TextColor { get; set; } - - [JsonProperty("background")] - public Color? Background { get; set; } - - [JsonProperty("borderColor")] - public Color? BorderColor { get; set; } - - [JsonProperty("focusDotColor")] - public Color? FocusDotColor { get; set; } - - [JsonProperty("borderSize")] - public double BorderSize { get; set; } = double.NaN; - - public static Theme DefaultBar() - { - return new Theme() - { - Background = Colors.White, - TextColor = Colors.Black, - BorderColor = Colors.Black, - BorderSize = 1 - }; - } - - /// - /// Default item theme. - /// - /// - public static Theme DefaultItem() - { - return new Theme() - { - Background = ColorConverter.ConvertFromString("#002957") as Color?, - TextColor = Colors.White - }; - } - - /// - /// Default control button theme. - /// - /// - public static Theme DefaultControl() - { - return new Theme() - { - Background = Color.FromRgb(0, 129, 69), - TextColor = Colors.White - }; - } - - /// - /// Sets the unset values of this instance using values of another. - /// - /// The instance to read values from. - /// true to set all values, false to set only values in this instance that are null. - public Theme Apply(Theme source, bool all = false) - { - foreach (PropertyInfo property in typeof(Theme).GetProperties().Where(p => p.CanWrite)) - { - object? origValue = all ? null : property.GetValue(this); - if (origValue is null || (origValue is double d && double.IsNaN(d))) - { - object? newValue = property.GetValue(source); - property.SetValue(this, newValue); - } - - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); - } - - return this; - } - - public event PropertyChangedEventHandler? PropertyChanged; - } -} \ No newline at end of file diff --git a/Morphic.Client/Bar/Data/BarJson.cs b/Morphic.Client/Bar/Data/BarJson.cs deleted file mode 100644 index 8b79e597..00000000 --- a/Morphic.Client/Bar/Data/BarJson.cs +++ /dev/null @@ -1,382 +0,0 @@ -// BarJson.cs: Bar deserialisation. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Reflection; - using System.Runtime.Serialization; - using Microsoft.Extensions.DependencyInjection; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Newtonsoft.Json.Serialization; - - public interface IDeserializable - { - public void Deserialized(); - } - - public static class BarJson - { - /// - /// Loads some json data. - /// - /// The service provider. - /// The input json. - /// An existing bar to populate. - /// - /// - public static T Load(IServiceProvider serviceProvider, TextReader reader, T? existingBar = null) - where T : class, IDeserializable, new() - { - - T? bar = existingBar ?? serviceProvider.GetService() ?? new T(); - - JsonSerializerSettings settings = new JsonSerializerSettings() - { - Context = new StreamingContext(StreamingContextStates.Other, bar), - Error = (sender, args) => - { - args.ToString(); - } - }; - - JsonSerializer jsonSerializer = JsonSerializer.Create(settings); - BarJsonTextReader barJsonTextReader = new BarJsonTextReader(reader, "win"); - - jsonSerializer.Populate(barJsonTextReader, bar); - - - bar?.Deserialized(); - - return bar!; - } - - /// - /// Customised JSON reader which handles platform specific fields. The platform for which a field is used, - /// is identified by a '$id' suffix. A field with a platform identifier of the current platform will be - /// used instead of one without. - /// - /// For example: - /// - /// "value": "default value", - /// "value$win": "windows-specific value", - /// "value$mac": "macOS-specific value - /// - /// - public class BarJsonTextReader : JsonTextReader - { - /// - /// Field paths visited which have the platform identifier. - /// - private readonly HashSet overridden = new HashSet(); - - public BarJsonTextReader(TextReader reader) : base(reader) - { - } - - public BarJsonTextReader(TextReader reader, string platformId) : this(reader) - { - this.PlatformId = platformId; - } - - public string PlatformId { get; } = "win"; - - public override object? Value - { - get - { - if (this.TokenType == JsonToken.PropertyName) - { - string name = base.Value?.ToString() ?? string.Empty; - string platformId = string.Empty; - string path = this.Path; - - // Take the platform identifier from the name. - if (name.Contains('$')) - { - string[]? parts = name.Split("$", 2); - if (parts.Length == 2) - { - name = parts[0]; - platformId = parts[1].ToLowerInvariant(); - path = path.Substring(0, path.Length - platformId.Length - 1); - } - } - - if (platformId == this.PlatformId) - { - // It's for this platform - use this field, and mark as over-ridden so it takes - // precedence over subsequent fields with no platform ID. - this.overridden.Add(path); - } - else if (platformId == string.Empty) - { - // No platform ID on this field name - use it only if there hasn't already been a - // field with a platform ID. - if (this.overridden.Contains(path)) - { - // Rename it so it's not used. - name = "_overridden:" + base.Value; - } - } - else - { - // Not for this platform - ignore this field. - name = "_ignored:" + base.Value; - } - - return name; - } - else - { - return base.Value; - } - } - } - } - } - - /// - /// Used by a class to specify, by name, the type of item it supports. - /// - [AttributeUsage(AttributeTargets.Class)] - public class JsonTypeNameAttribute : Attribute - { - public JsonTypeNameAttribute(string name) - { - this.Name = name; - } - - public string Name { get; } - } - - /// - /// Provides support for a polymorphic json object, while also allowing properties to deserialise with values - /// from a child object. - /// - /// The base class identifies the JSON field which specifies the type name via the 2nd parameter of the - /// JsonConverter attribute. - /// - /// The subclass specifies the type name which is supports via the JsonTypeName attribute. - /// - public class TypedJsonConverter : JsonConverter - { - private readonly string typeFieldName; - private readonly string defaultValue; - - public TypedJsonConverter(string typeFieldName, string defaultValue) - { - this.typeFieldName = typeFieldName; - this.defaultValue = defaultValue; - } - - /// - /// Creates an instance of the class inheriting baseType which has the JsonTypeName attribute - /// with the specified name. - /// - /// - /// The base type. - /// The name of the type. - /// - /// A class which inherits baseType. - private object? CreateInstance(JObject jObject, Type baseType, string name, BarData? barData) - { - // Find the class which has the JsonTypeName attribute with the given name. - Type? type = GetJsonType(baseType, name); - - if (type is null) - { - if (baseType.GetCustomAttributes().Any()) - { - // The type has already been resolved at the property. - type = baseType; - } - else - { - System.Diagnostics.Debug.Assert(false, $"Unable to get type of {baseType.Name} from '{this.typeFieldName} = ${name}'."); - return null; - } - } - - List ctorArgs = new List(); - bool gotCtor = false; - if (barData is not null) - { - ctorArgs.Add(barData); - - // Find a constructor of (BarData, string) - ConstructorInfo? ctor = type.GetConstructor(new[] { barData.GetType(), typeof(string) }); - if (ctor is not null) - { - // Get the property for the string argument. - ParameterInfo param = ctor.GetParameters().Last(); - JsonPropertyAttribute? attr = param.GetCustomAttribute(); - string? propertyName = attr?.PropertyName; - if (propertyName is not null) - { - gotCtor = true; - string? value = jObject.SelectToken(propertyName)?.ToString(); - ctorArgs.Add(value); - } - } - - // Find a constructor of (BarData) - if (!gotCtor) - { - gotCtor = type.GetConstructor(new[] { barData.GetType() }) is not null; - } - } - - object? instance = gotCtor - ? Activator.CreateInstance(type, ctorArgs.ToArray()) - : Activator.CreateInstance(type); - - if (instance is null) - { - throw new JsonSerializationException( - $"Unable to instantiate ${type.Name} from '${this.typeFieldName} = ${name}'."); - } - return instance; - } - - /// - /// Finds a type which is a subclass of baseType, having a JsonTypeName attribute with the specified name. - /// - /// The base class. - /// The name in the JsonTypeName attribute. - /// The type. - private static Type? GetJsonType(Type baseType, string name) - { - return baseType.Assembly.GetTypes() - .Where(t => !t.IsAbstract && t.IsSubclassOf(baseType)) - .FirstOrDefault(t => t.GetCustomAttribute()?.Name == name); - } - - /// - /// Gets the JSON field name for a given property, from either the JsonProperty attribute or the - /// name of the property. - /// - /// - /// - private static string GetFieldName(MemberInfo property) - { - JsonPropertyAttribute? attribute = property.GetCustomAttributes(true) - .FirstOrDefault(); - return attribute?.PropertyName ?? property.Name; - } - - /// - /// Instantiates the correct subclass of objectType, as identified by the type field. - /// - /// Also, makes the JsonProperty attribute allow a path into child objects. - /// - /// - /// - /// - /// - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, - JsonSerializer serializer) - { - JObject jo = JObject.Load(reader); - - BarPresets.Default.MergePreset(jo); - - BarData? bar = serializer.Context.Context as BarData; - - // Get the type of item. - string kindName = jo.SelectToken(this.typeFieldName)?.ToString() ?? this.defaultValue; - - // Create the class for the type. - object? target = this.CreateInstance(jo, objectType, kindName, bar); - if (target is null) - { - return null; - } - - // For each property, get the value using a path rather than just the field name. - // (inspired by https://stackoverflow.com/a/33094930/67586) - foreach (PropertyInfo property in target.GetType().GetProperties() - .Where(p => p.CanRead && p.CanWrite)) - { - // Get the value, using the path in the field name attribute. - string jsonPath = GetFieldName(property); - JToken? token = jo.SelectToken(jsonPath); - - if (token is not null && token.Type != JTokenType.Null) - { - Type? newType = this.GetNewType(jo, property); - object? value = newType is null - ? token.ToObject(property.PropertyType, serializer) - : token.ToObject(newType); - // Set the property value. - property.SetValue(target, value, null); - } - } - - return target; - } - - /// - /// Gets the actual type to use, from the property. - /// - /// The current json object to look at. - /// The property. - /// The type to use, or null to use the property's own type - private Type? GetNewType(JObject jo, PropertyInfo property) - { - JsonConverterAttribute? converter = property.GetCustomAttribute() - ?? property.PropertyType.GetCustomAttribute(); - if (converter?.ConverterParameters is null || converter.ConverterType != this.GetType()) - { - return null; - } - - string nameField = (string) converter.ConverterParameters[0]; - string defaultValue = (string) converter.ConverterParameters[1]; - string name = jo[nameField]?.ToString() ?? defaultValue; - return TypedJsonConverter.GetJsonType(property.PropertyType, name); - - } - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - // This isn't worth writing - the client only consumes JSON. - throw new NotImplementedException(); - } - - public override bool CanConvert(Type objectType) - { - throw new NotImplementedException(); - } - } - - /// Contract resolver to allow private properties to be deserialised. - internal class BarJsonContractResolver : DefaultContractResolver - { - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty jsonProperty = base.CreateProperty(member, memberSerialization); - - // Allow private members to be deserialised. - if (!jsonProperty.Writable && member is PropertyInfo propertyInfo) - { - jsonProperty.Writable = propertyInfo.GetSetMethod(true) is not null; - } - - return jsonProperty; - } - } - -} diff --git a/Morphic.Client/Bar/Data/BarMultiButton.cs b/Morphic.Client/Bar/Data/BarMultiButton.cs deleted file mode 100644 index ca222346..00000000 --- a/Morphic.Client/Bar/Data/BarMultiButton.cs +++ /dev/null @@ -1,168 +0,0 @@ -// BarMultiButton.cs: Bar item containing multiple buttons. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - - -namespace Morphic.Client.Bar.Data -{ - using System.Collections.Generic; - using System.Text.RegularExpressions; - using Actions; - using Newtonsoft.Json; - using UI.BarControls; - - /// - /// Bar item that contains multiple buttons. - /// - [JsonTypeName("multi")] - [BarControl(typeof(MultiButtonBarControl))] - public class BarMultiButton : BarItem - { - public BarMultiButton(BarData bar) : base(bar) - { - - } - - [JsonProperty("configuration.buttons")] - public Dictionary Buttons { get; set; } = new Dictionary(); - - [JsonProperty("configuration.autoSize")] - public bool AutoSize { get; set; } - - /// - /// Provides hints for keyboard usage. - /// - [JsonProperty("configuration.type")] - public MultiButtonType Type { get; set; } = MultiButtonType.Auto; - - - [JsonObject(MemberSerialization.OptIn)] - public class ButtonInfo - { - private string? value; - private string? uiName; - private string? text; - public BarMultiButton BarItem { get; internal set; } = null!; - - /// - /// Display text. - /// - [JsonProperty("label")] - public string Text - { - get => this.text ?? string.Empty; - set => this.text = value; - } - - /// - /// Unique identifier. Of omitted, the key from BarMultiButton.Buttons is used. - /// - [JsonProperty("id")] - public string Id { get; set; } = null!; - - /// - /// The value to pass to the action when this button is clicked. - /// Used by `kind = "internal"`, specifying "{button}" as an argument value will resolve to this value - /// (or the id, if not set) - /// - [JsonProperty("value")] - public string Value - { - get => this.value ?? this.Id; - set => this.value = value; - } - - [JsonProperty("action")] - public BarAction? Action { get; set; } - - [JsonProperty("tooltip")] - public string? Tooltip { get; set; } - - [JsonProperty("menu")] - public Dictionary Menu { get; set; } = new Dictionary(); - - [JsonProperty("telemetryCategory")] - public string? TelemetryCategory { get; set; } - - [JsonProperty("uiName")] - public string UiName - { - get - { - string value = this.uiName ?? this.text ?? this.Tooltip ?? string.Empty; - value = value switch - { - "+" => "up", - "-" => "down", - _ => value - }; - return value; - } - set => this.uiName = value; - } - - public bool Toggle { get; set; } - } - - public override void Deserialized() - { - base.Deserialized(); - - foreach (var (key, buttonInfo) in this.Buttons) - { - if ((buttonInfo.Action is null) || (buttonInfo.Action is NoOpAction)) - { - buttonInfo.Action = this.Action; - } - buttonInfo.BarItem = this; - buttonInfo.UiName = this.UiName + " " + buttonInfo.UiName; - if (string.IsNullOrEmpty(buttonInfo.Id)) - { - buttonInfo.Id = key; - } - } - - if (this.Type == MultiButtonType.Auto) - { - this.Type = MultiButtonType.Buttons; - if (this.Buttons.Count == 2) - { - // Detect if it's an additive/toggle button pair, based on the text - Regex additive = new Regex("^([-+]|in|out|up|down|(in|de)c(rease)?)$", RegexOptions.IgnoreCase); - Regex toggle = new Regex("^(on|off|yes|no|true|false|(en|dis)abled?)$", RegexOptions.IgnoreCase); - - foreach (ButtonInfo buttonInfo in this.Buttons.Values) - { - if (additive.IsMatch(buttonInfo.Text) || additive.IsMatch(buttonInfo.Value)) - { - this.Type = MultiButtonType.Additive; - break; - } - else if (toggle.IsMatch(buttonInfo.Text) || additive.IsMatch(buttonInfo.Value)) - { - this.Type = MultiButtonType.Toggle; - break; - } - } - } - } - } - } - - public enum MultiButtonType - { - Auto, - /// Just buttons - Buttons, - /// -/+ - Additive, - /// On/Off - Toggle - } -} diff --git a/Morphic.Client/Bar/Data/BarPosition.cs b/Morphic.Client/Bar/Data/BarPosition.cs deleted file mode 100644 index 8a868283..00000000 --- a/Morphic.Client/Bar/Data/BarPosition.cs +++ /dev/null @@ -1,370 +0,0 @@ -// BarPosition.cs: The initial positioning of a bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.Reflection; - using System.Runtime.CompilerServices; - using System.Windows; - using System.Windows.Controls; - using Newtonsoft.Json; - using Newtonsoft.Json.Converters; - using UI.AppBarWindow; - - /// - /// The position of a bar. - /// - [JsonObject(MemberSerialization.OptIn)] - public class BarPosition - { - private ExpanderRelative expanderRelative = ExpanderRelative.Both; - private const string LEFT = "left"; - private const string TOP = "top"; - private const string RIGHT = "right"; - private const string BOTTOM = "bottom"; - private const string MIDDLE = "middle"; - - /// - /// The side of the screen where the bar will be docked, reserving the desktop work area. - /// "disable" will prevent the user from docking it. - /// - [JsonProperty("docked", NullValueHandling = NullValueHandling.Ignore)] - private string Docked - { - set - { - if (value.StartsWith("disable", StringComparison.InvariantCultureIgnoreCase)) - { - this.AllowDocking = false; - this.DockEdge = Edge.None; - } - else if (Enum.TryParse(value, true, out Edge edge)) - { - this.DockEdge = edge; - } - } - } - - /// - /// The side of the screen where the bar will be docked, reserving the desktop work area. - /// - public Edge DockEdge { get; set; } = Edge.None; - - /// Bar can be docked to the edges (like the taskbar). - public bool AllowDocking { get; private set; } = true; - - /// Bar is restricted to the screen corners. - [JsonProperty("restricted")] - public bool Restricted { get; private set; } - - /// - /// The horizontal position of the bar. Can be "Left", "Middle", "Right", a number, or a percentage. - /// Numbers or percentages can be negative (including -0), meaning distance from the right. - /// Percentages specify the position of the middle of the bar. - /// - [JsonProperty("x")] - public string? PrimaryXValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The vertical position of the bar. Can be "Top", "Middle", "Bottom", a number, or a percentage. - /// Numbers or percentages can be negative (including -0), meaning distance from the bottom. - /// Percentages specify the position of the middle of the bar. - /// - [JsonProperty("y")] - public string? PrimaryYValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The initial orientation of the bar. Ignored if docked. - /// - [JsonProperty("horizontal")] - public bool Horizontal - { - get => this.Orientation == System.Windows.Controls.Orientation.Horizontal; - set => this.Orientation = - value ? System.Windows.Controls.Orientation.Horizontal : System.Windows.Controls.Orientation.Vertical; - } - - public Orientation Orientation { get; set; } = Orientation.Vertical; - - /// - /// The horizontal/vertical position of the secondary bar, relative to the primary bar (when above or below it). - /// - [JsonProperty("secondary")] - public string? SecondaryXyValues - { - set - { - this.SecondaryXValue = value; - this.SecondaryYValue = value; - } - } - - /// - /// The horizontal position of the secondary bar, relative to the primary bar (when above or below it). - /// - [JsonProperty("secondaryX")] - public string? SecondaryXValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The vertical position of the secondary bar, relative to the primary bar (when beside it). - /// - [JsonProperty("secondaryY")] - public string? SecondaryYValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The horizontal/vertical position of the expander button bar - /// - [JsonProperty("expander")] - public string? ExpanderXyValues - { - set - { - this.ExpanderXValue = value; - this.ExpanderYValue = value; - } - } - - /// - /// The horizontal position of the expander button bar - /// - [JsonProperty("expanderX")] - public string? ExpanderXValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The vertical position of the secondary bar - /// - [JsonProperty("expanderY")] - public string? ExpanderYValue - { - set => this.ParsePosition(value ?? "0"); - } - - /// - /// The bar that the expander position is relative to. - /// - [JsonProperty("expanderRelative")] - [JsonConverter(typeof(StringEnumConverter))] - public ExpanderRelative ExpanderRelative - { - set => this.expanderRelative = value; - get => this.expanderRelative; - } - - public RelativePosition Primary { get; set; } = new RelativePosition(); - public RelativePosition Secondary { get; set; } = new RelativePosition(); - public RelativePosition Expander { get; set; } = new RelativePosition(); - - /// - /// Gets the AxisPosition for the given json property. - /// - /// Name of the json property (xxValue). - /// The AxisPosition. - private AxisPosition GetAxisPositionFromName(string jsonPropertyName) - { - string backingPropertyName = jsonPropertyName.Substring(0, jsonPropertyName.Length - "XValue".Length); - - PropertyInfo property = this.GetType().GetProperty(backingPropertyName) - ?? throw new ArgumentException( - $"json property '{jsonPropertyName}' has no backing property.", - nameof(jsonPropertyName)); - RelativePosition axisPosition = (property.GetValue(this) as RelativePosition)!; - - string axis = jsonPropertyName.Substring(backingPropertyName.Length, 1); - return axis switch - { - "X" => axisPosition.X, - "Y" => axisPosition.Y, - _ => throw new InvalidOperationException($"Unable to get axis from property name {jsonPropertyName}") - }; - } - - /// - /// A tuple, for "X and Y" values. - /// - /// - public class RelativePosition - { - public AxisPosition X { get; set; } = new AxisPosition(); - public AxisPosition Y { get; set; } = new AxisPosition(); - - /// - /// Gets the desired initial position of a window, relative to the work area. - /// - /// The work-area for the window. - /// The window size. - /// true if setting the position of the secondary bar. - /// The location of the window. - public Point GetPosition(Rect workArea, Size size) - { - Rect result = new Rect() - { - X = this.X.GetAbsolute(workArea.Left, workArea.Right, size.Width), - Y = this.Y.GetAbsolute(workArea.Top, workArea.Bottom, size.Height), - Size = size - }; - - return result.Location; - } - } - - public class AxisPosition - { - /// - /// The value. - /// - public double Value { get; set; } - /// - /// true if the number is a 0-1 percentage. - /// - public bool IsRelative { get; set; } - - /// - /// Returns -1 or 1, depending on its sign (including -/+ zero). - /// - public int Sign => this.Value == 0.0 - ? double.IsNegativeInfinity(1.0 / this.Value) ? -1 : 1 - : Math.Sign(this.Value); - - public bool IsNegative => this.Sign < 0; - - public AxisPosition() - { - } - - public AxisPosition(double value, bool isRelative) - { - this.Value = value; - this.IsRelative = isRelative; - } - - public static implicit operator double(AxisPosition axisPosition) - { - return axisPosition.Value; - } - - /// - /// Get the absolute position of this position, relative to the given range. - /// - /// The minimum value of the range. - /// The maximum value of the range. - /// true to ensure the result is witihn the range. - /// - public double GetAbsolute(double min, double max, bool clamp = true) - { - // Negative values are taken from the max. - double offset = this.IsNegative ? max : min; - - double result = this.IsRelative - ? offset + this.Value * (max - min) - : offset + this.Value; - - return clamp - ? Math.Clamp(result, min, max) - : result; - } - - public double GetAbsolute(double min, double max, double size) - { - double value = 0; - if (min > max - size) - { - // The outer area is smaller than the inner - if (this.IsRelative) - { - double v = Math.Abs(this.Value); - if (v < 0.501) - { - // left/top - value = min; - } - else - { - // right/bottom - value = max - size; - } - } - else - { - value = this.IsNegative ? max - size : min; - } - } - else - { - value = this.GetAbsolute(min, max - size); - - if (this.IsRelative) - { - //value -= size / 2; - } - - value = Math.Clamp(value, min, max - size); - } - - return value; - } - } - - private void ParsePosition(string origValue, [CallerMemberName] string propertyName = "") - { - if (!propertyName.EndsWith("Value")) - { - throw new ArgumentException("Property name should end with 'Value'", nameof(propertyName)); - } - - string value = origValue.Trim().ToLowerInvariant() switch - { - LEFT => "0", - TOP => "0", - RIGHT => "-0", - BOTTOM => "-0", - MIDDLE => "50%", - _ => origValue - }; - - bool relative = value.EndsWith("%"); - if (relative) - { - value = value.Substring(0, value.Length - 1); - } - - double num; - if (!double.TryParse(value, out num)) - { - throw new JsonException($"{propertyName}: Unrecognised positional value '{value}'."); - } - - if (relative) - { - num /= 100; - } - - // Set the backing property. - AxisPosition axisPosition = this.GetAxisPositionFromName(propertyName); - axisPosition.Value = num; - axisPosition.IsRelative = relative; - } - } -} diff --git a/Morphic.Client/Bar/Data/BarPresets.cs b/Morphic.Client/Bar/Data/BarPresets.cs deleted file mode 100644 index fc7f45d9..00000000 --- a/Morphic.Client/Bar/Data/BarPresets.cs +++ /dev/null @@ -1,112 +0,0 @@ -// BarActions.cs: Deserialised presets.json5. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System.Collections.Generic; - using System.IO; - using Config; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - - /// - /// Deserialised presets.json5. - /// - [JsonObject(MemberSerialization.OptIn)] - public class BarPresets : IDeserializable - { - public static readonly BarPresets Default = BarPresets.FromFile(AppPaths.GetConfigFile("presets.json5", true)); - - [JsonProperty("actions")] - public JsonDict Actions { get; set; } = new JsonDict(); - - [JsonProperty("defaults")] - public JsonDict Defaults { get; set; } = new JsonDict(); - - public static BarPresets FromFile(string file) - { - using StreamReader? reader = File.OpenText(file); - return BarJson.Load(App.Current.ServiceProvider, reader); - } - - /// - /// Gets the parsed json object for the given identifier. - /// - /// - /// - public static JObject? GetActionObject(string identifier) - { - BarPresets.Default.Actions.TryGetValue(identifier, out JObject? jo); - return (JObject?)jo?.DeepClone(); - } - - public void Deserialized() - { - } - - /// - /// Merges a preset into a given JSON object. - /// - /// For bar items that are of kinds "action" or "application", respectively the "identifier" or "default" fields - /// of their configuration block are used as a lookup in the appropriate dictionary in this class. - /// - /// The object found in the lookup is then merged over the original. - /// - /// This is performed during deserialisation, just before the class instantiation so they are unaware of - /// such hackery. - /// - /// The BarItem JSON object. - /// - public JObject MergePreset(JObject jo) - { - string? kind = jo.SelectToken("kind")?.ToString(); - bool isAction = kind == "action"; - bool isApplication = kind == "application"; - - string? key = null; - if (isAction || isApplication) - { - string? keyField = isAction ? "configuration.identifier" : "configuration.default"; - key = jo.SelectToken(keyField)?.ToString(); - } - - if (!string.IsNullOrEmpty(key)) - { - JsonDict dict = isAction ? this.Actions : this.Defaults; - - dict.TryGetValue(key, out JObject? preset); - - if (preset is not null) - { - jo.Merge(preset.DeepClone()); - } - } - - // Make "kind = setting" items imply "widget = setting" - string? newKind = jo.SelectToken("kind")?.ToString(); - if (newKind == "setting") - { - if (!jo.ContainsKey("widget")) - { - jo.Add("widget", "setting"); - } - } - - return jo; - } - } - - /// - /// A dictionary of JSON objects. - /// - public class JsonDict : Dictionary - { - } -} diff --git a/Morphic.Client/Bar/Data/BarSettingItem.cs b/Morphic.Client/Bar/Data/BarSettingItem.cs deleted file mode 100644 index 667ef8aa..00000000 --- a/Morphic.Client/Bar/Data/BarSettingItem.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace Morphic.Client.Bar.Data -{ - using Microsoft.Extensions.DependencyInjection; - using Newtonsoft.Json; - using Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - using UI.BarControls; - - /// - /// A bar item that handles a setting in the solutions registry. - /// - [JsonTypeName("setting")] - [BarControl(typeof(MultiButtonBarControl))] - public class BarSettingItem : BarMultiButton - { - [JsonProperty("configuration.settingId", Required = Required.Always)] - public string? SettingId { get; } - - public Solutions Solutions { get; private set; } - - public BarSettingItem(BarData bar, [JsonProperty("configuration.settingId")] string settingId) : base(bar) - { - this.Solutions = this.Bar.ServiceProvider.GetRequiredService(); - this.SettingId = settingId; - - this.ApplySetting(); - } - - private void ApplySetting() - { - if (!string.IsNullOrEmpty(this.SettingId)) - { - // Bar item is a pair of on/off or up/down buttons for a single setting. - Setting setting = this.Solutions.GetSetting(this.SettingId); - - if (setting.Range is not null) - { - this.Type = MultiButtonType.Additive; - this.Buttons["dec"] = new ButtonInfo() - { - Text = "-" - }; - this.Buttons["inc"] = new ButtonInfo() - { - Text = "+" - }; - } - else - { - this.Type = MultiButtonType.Toggle; - this.Buttons["on"] = new ButtonInfo() - { - Text = "On" - }; - this.Buttons["off"] = new ButtonInfo() - { - Text = "Off" - }; - } - } - } - - private void ToggleButtons() - { - foreach ((string? key, ButtonInfo? value) in this.Buttons) - { - value.Toggle = true; - } - } - - public override void Deserialized() - { - base.Deserialized(); - if (string.IsNullOrEmpty(this.SettingId)) - { - this.ToggleButtons(); - } - - } - } -} diff --git a/Morphic.Client/Bar/Data/BarSizes.cs b/Morphic.Client/Bar/Data/BarSizes.cs deleted file mode 100644 index d26ac782..00000000 --- a/Morphic.Client/Bar/Data/BarSizes.cs +++ /dev/null @@ -1,115 +0,0 @@ -// BarSizes.cs: Sizing configuration for the bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using System; - using System.ComponentModel; - using System.Windows; - using Newtonsoft.Json; - - [JsonObject(MemberSerialization.OptIn)] - public class BarSizes - { - /// Padding between edge of bar and items. - [JsonProperty("windowPadding")] - public Thickness WindowPadding { get; set; } = new Thickness(0); - - /// Spacing between items. - [JsonProperty("itemSpacing")] - public double ItemSpacing { get; set; } = 1; - - /// Item width. - [JsonProperty("itemWidth")] - public double ItemWidth { get; set; } = 100; - - /// Maximum Button Item title lines. - [JsonProperty("buttonTextLines")] - public int ButtonTextLines { get; set; } = 2; - - /// - /// Button Item padding between edge and title. And for the top, between circle and title - /// - [JsonProperty("buttonPadding")] - public Thickness ButtonTextPadding { get; set; } = new Thickness(10); - - /// Button Item circle image diameter (a fraction relative to the itemWidth). - [JsonProperty("buttonCircleDiameter")] - public double ButtonCircleDiameterField { get; set; } = 0.666d; - - /// Button Item circle image diameter. - public double ButtonCircleDiameter => Math.Floor(this.ItemWidth * this.ButtonCircleDiameterField); - - public double ButtonImageSize => - Math.Sqrt(Math.Pow(this.ButtonCircleDiameter - this.CircleBorderWidth * 4, 2) / 2); - - /// Button Item circle overlap with rectangle (a fraction relative to buttonImageSize). - [JsonProperty("buttonImageOverlap")] - public double ButtonImageOverlapField { get; set; } = 0.333d; - - /// Button Item circle overlap with rectangle. - public double ButtonImageOverlap => Math.Floor(this.ButtonCircleDiameter * this.ButtonImageOverlapField); - /// Space between the top of the image and the button rectangle. - public double ButtonImageOffset => Math.Floor(this.ButtonCircleDiameter * (1 - this.ButtonImageOverlapField)); - - [JsonProperty("buttonFontSize")] - public double ButtonFontSize { get; set; } = 14; - - [JsonProperty("buttonFontWeight")] - public FontWeight ButtonFontWeight { get; set; } = FontWeights.Normal; - - [JsonProperty("circleBorderWidth")] - public double CircleBorderWidth { get; set; } = 2; - - [JsonProperty("buttonCornerRadius")] - public double ButtonCornerRadius { get; set; } = 10; - - /// Size of the label text. - [JsonProperty("controlLabelFontSize")] - public double ControlLabelFontSize { get; set; } = 14; - - /// Weight of the label text. - [JsonProperty("controlLabelFontWeight")] - public FontWeight ControlLabelFontWeight { get; set; } = FontWeights.Bold; - - /// Space around the label. - [JsonProperty("controlLabelMargin")] - public Thickness ControlLabelMargin { get; set; } = new Thickness(0, 5, 0, 5); - - public Thickness ControlButtonMargin { get; set; } = new Thickness(0.5); - - /// Size of the button text. - [JsonProperty("controlButtonFontSize")] - public double ControlButtonFontSize { get; set; } = 14; - - /// Weight of the button text. - [JsonProperty("controlButtonFontWeight")] - public FontWeight ControlButtonFontWeight { get; set; } = FontWeights.Bold; - - /// Space within the button. - [JsonProperty("controlButtonPadding")] - public double ControlButtonPaddingValue - { - get => this.ControlButtonPadding.Left; - set => this.ControlButtonPadding = new Thickness(value, 0, value, 0); - } - - public Thickness ControlButtonPadding { get; set; } = new Thickness(5, 5, 5, 5); - - [JsonProperty("controlCornerRadius")] - public double ControlCornerRadius { get; set; } = 5; - - [TypeConverter(typeof(LengthConverter))] - [JsonProperty("controlButtonHeight")] - public double ControlButtonHeight { get; set; } = 30; - - - } -} diff --git a/Morphic.Client/Bar/Data/SecondaryBar.cs b/Morphic.Client/Bar/Data/SecondaryBar.cs deleted file mode 100644 index 899b9a69..00000000 --- a/Morphic.Client/Bar/Data/SecondaryBar.cs +++ /dev/null @@ -1,34 +0,0 @@ -// SecondaryBar.cs: Configuration options for the secondary bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.Data -{ - using Newtonsoft.Json; - - /// - /// Options specific to the secondary bar. - /// - [JsonObject(MemberSerialization.OptIn)] - public class SecondaryBar - { - /// - /// Hide the secondary bar when another application gains focus. - /// - [JsonProperty("autohide")] - public bool AutoHide { get; set; } - - /// - /// Hide the pull-out button when another application gains focus. - /// - [JsonProperty("autohideExpander")] - public bool AutoHideExpander { get; set; } - - } -} diff --git a/Morphic.Client/Bar/UI/AppBarWindow/AppBar.cs b/Morphic.Client/Bar/UI/AppBarWindow/AppBar.cs deleted file mode 100644 index 8b8f6682..00000000 --- a/Morphic.Client/Bar/UI/AppBarWindow/AppBar.cs +++ /dev/null @@ -1,502 +0,0 @@ -// AppBar.cs: Lets a window be dragged and docked. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.AppBarWindow -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Input; - - /// - /// Makes a Window become a draggable "app bar" window which can be snapped or docked to the desktop edges. - /// - public class AppBar - { - private readonly Window window; - private readonly WindowMovement windowMovement; - private readonly AppBarApi api; - - private Point mouseDownPos; - private Size floatingSize = Size.Empty; - public Edge AppBarEdge { get; private set; } = Edge.None; - public bool EnableDocking { get; set; } = true; - - public bool SnapToEdges { get; set; } = true; - public bool Draggable { get; set; } = true; - public bool FixedOrientation { get; set; } - - - public event EventHandler? EdgeChanged; - public event EventHandler? BeginDragMove; - - public AppBar(Window window) : this(window, new WindowMovement(window)) - { - } - - public AppBar(Window window, WindowMovement windowMovement) - { - if (!(window is IAppBarWindow)) - { - throw new ArgumentException($"The window must implement {nameof(IAppBarWindow)}.", nameof(window)); - } - - this.window = window; - this.windowMovement = windowMovement; - this.api = new AppBarApi(this.windowMovement); - - // Make the window draggable. - this.window.PreviewMouseDown += this.OnPreviewMouseDown; - this.window.PreviewMouseMove += this.OnPreviewMouseMove; - - this.windowMovement.MoveComplete += this.OnMoveComplete; - - this.windowMovement.Moving += this.OnMoving; - - this.window.Closed += (sender, args) => this.ApplyAppBar(Edge.None); - } - - /// - /// Gets a size which better fits the content. - /// - /// The suggested size. - /// - /// true if the size is in pixels. - /// The new size. - public Size GetGoodSize(Size size, Orientation orientation, bool inPixels = false) - { - Rect workArea = this.FromPixels(this.windowMovement.GetWorkArea()); - - Size newSize = ((IAppBarWindow)this.window).GetSize(inPixels ? this.FromPixels(size) : size, orientation, workArea); - newSize.Width = Math.Min(newSize.Width, workArea.Width); - newSize.Height = Math.Min(newSize.Height, workArea.Height); - - return (inPixels ? this.ToPixels(newSize) : newSize); - } - - /// - /// Called when the window has stopped being moved or sized. - /// - /// - /// - private void OnMoveComplete(object? sender, EventArgs args) - { - // Reserve desktop space for the window. - this.ApplyAppBar(this.AppBarEdge); - } - - protected virtual void OnBeginDragMove(CancelableEventArgs e) - { - this.BeginDragMove?.Invoke(this, e); - } - - - public void ApplyAppBar(Edge edge, bool preview = false) - { - if (!preview) - { - this.api.Apply(edge); - } - - this.OnEdgeChanged(edge, preview); - } - - /// - /// Adjusts a thickness so the edges that are touching the screen edge are zero. - /// - /// The initial thickness. - /// true to remove on the non-touching edge. - /// Value of "zero" thickness. - public Thickness AdjustThickness(Thickness thickness, bool invert = false, Thickness? none = null) - { - none ??= new Thickness(0); - - if (this.AppBarEdge == Edge.None) - { - return invert ? none.Value : thickness; - } - - Edge notTouching = this.AppBarEdge.Opposite(); - - Dictionary actions = new Dictionary() - { - {Edge.Left, () => thickness.Left = none.Value.Left }, - {Edge.Right, () => thickness.Right = none.Value.Right }, - {Edge.Top, () => thickness.Top = none.Value.Top }, - {Edge.Bottom, () => thickness.Bottom = none.Value.Bottom } - }; - - foreach ((Edge edge, Action action) in actions) - { - if ((edge == notTouching) == invert) - { - action.Invoke(); - } - } - - return thickness; - } - - /// - /// Called when the window is being moved, to re-adjust the window in-flight. - /// - /// - /// - private void OnMoving(object? sender, WindowMovement.MovementEventArgs args) - { - args.Handled = true; - - if (args.IsFirst) - { - if (this.AppBarEdge == Edge.None) - { - this.floatingSize = args.Rect.Size; - } - else - { - // Un-dock the window so it can be moved. - this.ApplyAppBar(Edge.None); - - // Revert to the original size - args.Rect.Size = this.floatingSize; - - // If the window is not under the cursor, move it so the cursor is in the centre. - if (args.Rect.X > args.Cursor.X || args.Rect.Right < args.Cursor.X) - { - args.Rect.X = args.Cursor.X - args.Rect.Width / 2; - } - - if (args.Rect.Y > args.Cursor.Y || args.Rect.Bottom < args.Cursor.Y) - { - args.Rect.Y = args.Cursor.Y - args.Rect.Height / 2; - } - - // Make it look like the window was this size when the move started. - args.InitialRect = args.NewInitialRect = args.Rect; - } - } - - // Like magnifier, if the mouse pointer is on the edge then make the window an app bar on that edge. - Point mouse = WindowMovement.GetCursorPos(); - Rect workArea = this.windowMovement.GetWorkArea(new Point(mouse.X, mouse.Y)); - if (this.EnableDocking) - { - // See what edge the mouse is near (or beyond) - Rect mouseRect = new Rect( - Math.Clamp(mouse.X, workArea.Left, workArea.Right), - Math.Clamp(mouse.Y, workArea.Top, workArea.Bottom), 0, 0); - - Edge lastEdge = this.AppBarEdge; - this.AppBarEdge = NearEdges(workArea, mouseRect, 5).First(); - if (lastEdge != this.AppBarEdge) - { - this.OnEdgeChanged(this.AppBarEdge, true); - } - } - - // Reposition the window to fit the edge. - switch (this.AppBarEdge) - { - case Edge.Left: - case Edge.Right: - args.Rect.Height = workArea.Height; - args.Rect.Width = this.GetGoodSize(new Size(50, args.Rect.Height), Orientation.Vertical, true).Width; - args.Rect.Y = workArea.Top; - if (this.AppBarEdge == Edge.Left) - { - args.Rect.X = workArea.X; - } - else - { - args.Rect.X = workArea.Right - args.Rect.Width; - } - - this.window.SizeToContent = SizeToContent.Width; - - break; - - case Edge.Top: - case Edge.Bottom: - args.Rect.Width = workArea.Width; - args.Rect.Height = this.GetGoodSize(new Size(args.Rect.Width, 50), Orientation.Horizontal, true).Height; - args.Rect.X = workArea.X; - if (this.AppBarEdge == Edge.Top) - { - args.Rect.Y = workArea.Y; - } - else - { - args.Rect.Y = workArea.Bottom - args.Rect.Height; - } - - this.window.SizeToContent = SizeToContent.Height; - break; - - case Edge.None: - args.Rect = args.SupposedRect; - // Snap to an edge - if (this.SnapToEdges) - { - this.SnapToEdge(this.windowMovement.GetWorkArea(), ref args.Rect, 20); - } - - this.window.SizeToContent = SizeToContent.WidthAndHeight; - break; - } - } - - /// - /// The width and height of the Window when it is docked. - /// - public Size DockedSizes { get; set; } = new Size(100, 100); - - public Size ToPixels(Size size) => (Size)this.ToPixels((Point) size); - public Size FromPixels(Size size) => (Size)this.FromPixels((Point) size); - - public Point ToPixels(Point point) - { - return PresentationSource.FromVisual(this.window)?.CompositionTarget.TransformToDevice.Transform(point) - ?? point; - } - - public Point FromPixels(Point point) - { - return PresentationSource.FromVisual(this.window)?.CompositionTarget.TransformFromDevice.Transform(point) - ?? point; - } - - public Rect ToPixels(Rect rect) - { - return new Rect(this.ToPixels(rect.Location), this.ToPixels(rect.Size)); - } - - public Rect FromPixels(Rect rect) - { - return new Rect(this.FromPixels(rect.Location), this.FromPixels(rect.Size)); - } - - public Size DockedSizesPixels { get; set; } - - /// - /// Snaps a rectangle to the edges of another, if it's close enough. - /// - /// The outer rectangle to check against. - /// The inner rect to adjust. - /// The distance that the edge can be, in order to snap. - private void SnapToEdge(Rect outer, ref Rect rect, double distance) - { - HashSet edges = NearEdges(outer, rect, distance); - if (!edges.Contains(Edge.None)) - { - if (edges.Contains(Edge.Left)) - { - rect.X = outer.X; - } - - if (edges.Contains(Edge.Top)) - { - rect.Y = outer.Y; - } - - if (edges.Contains(Edge.Right)) - { - rect.X = outer.Right - rect.Width; - } - - if (edges.Contains(Edge.Bottom)) - { - rect.Y = outer.Bottom - rect.Height; - } - } - } - - /// - /// Determines the edges of a rectangle that are close to the edge of an outer rectangle. - /// - /// The outer rectangle. - /// The inner rectangle. - /// The distance that the edges need to be in order to be near. - /// Set of edges that are close. Will contain only Edge.None if no edges are close. - private static HashSet NearEdges(Rect outer, Rect rect, double distance) - { - HashSet result = new HashSet(); - - bool Near(double a, double b) => Math.Abs(a - b) <= distance; - - if (Near(outer.X, rect.X)) - { - result.Add(Edge.Left); - } - else if (Near(rect.Right, outer.Right)) - { - result.Add(Edge.Right); - } - - if (Near(outer.Y, rect.Y)) - { - result.Add(Edge.Top); - } - else if (Near(rect.Bottom, outer.Bottom)) - { - result.Add(Edge.Bottom); - } - - if (result.Count == 0) - { - result.Add(Edge.None); - } - - return result; - } - - - /// - /// Keeps an eye on the mouse movement. If it's a drag action, start the window move. - /// - /// - /// - private void OnPreviewMouseMove(object sender, MouseEventArgs args) - { - if (this.Draggable && args.LeftButton == MouseButtonState.Pressed) - { - Point point = args.GetPosition(this.window); - - if (Math.Abs(point.X - this.mouseDownPos.X) >= SystemParameters.MinimumHorizontalDragDistance || - Math.Abs(point.Y - this.mouseDownPos.Y) >= SystemParameters.MinimumVerticalDragDistance) - { - CancelableEventArgs eventArgs = new CancelableEventArgs(); - this.OnBeginDragMove(eventArgs); - if (!eventArgs.Cancel) - { - this.windowMovement.DragMove(); - } - } - } - } - - /// - /// Stores the point at which the mouse was pressed, in order to determine if a move becomes a drag. - /// - /// - /// - private void OnPreviewMouseDown(object sender, MouseButtonEventArgs args) - { - if (this.Draggable && args.LeftButton == MouseButtonState.Pressed) - { - this.mouseDownPos = args.GetPosition(this.window); - } - } - - protected virtual void OnEdgeChanged(Edge edge, bool preview) - { - this.OnEdgeChanged(new EdgeChangedEventArgs(edge, preview)); - } - - protected virtual void OnEdgeChanged(EdgeChangedEventArgs args) - { - this.EdgeChanged?.Invoke(this, args); - } - } - - public interface IAppBarWindow - { - public Size GetSize(Size availableSize, Orientation orientation, Rect workArea); - } - - public class EdgeChangedEventArgs : EventArgs - { - public EdgeChangedEventArgs(Edge edge, bool preview) - { - this.Edge = edge; - this.Preview = preview; - } - - /// - /// The edge of the screen. - /// - public Edge Edge { get; } - /// - /// true if the current change is only a preview, the desktop reservation has not yet been applied. - /// - public bool Preview { get; } - } - - public static class AppBarExtensionMethods - { - public static Edge Opposite(this Edge edge) - { - return edge switch - { - Edge.None => Edge.None, - Edge.Left => Edge.Right, - Edge.Top => Edge.Bottom, - Edge.Right => Edge.Left, - Edge.Bottom => Edge.Top, - _ => Edge.None - }; - } - - public static Orientation Opposite(this Orientation orientation) - { - return orientation == Orientation.Horizontal - ? Orientation.Vertical - : Orientation.Horizontal; - } - - /// - /// Is the edge horizontal? top or bottom. - /// - /// - /// - public static bool IsHorizontal(this Edge edge) - { - return (edge == Edge.Top || edge == Edge.Bottom); - } - - /// - /// Is the edge vertical? left or right. - /// - /// - /// - public static bool IsVertical(this Edge edge) - { - return (edge == Edge.Left || edge == Edge.Right); - } - - public static Orientation? GetOrientation(this Edge edge) - { - if (edge.IsHorizontal()) - { - return Orientation.Horizontal; - } - else if (edge.IsVertical()) - { - return Orientation.Vertical; - } - else - { - return null; - } - } - - public static Rect GetRect(this Window window) - { - return new Rect(window.Left, window.Top, window.Width, window.Height); - } - } - - public class CancelableEventArgs : EventArgs - { - public bool Cancel { get; set; } - } - -} \ No newline at end of file diff --git a/Morphic.Client/Bar/UI/AppBarWindow/AppBarAPI.cs b/Morphic.Client/Bar/UI/AppBarWindow/AppBarAPI.cs deleted file mode 100644 index 0ba91b4c..00000000 --- a/Morphic.Client/Bar/UI/AppBarWindow/AppBarAPI.cs +++ /dev/null @@ -1,236 +0,0 @@ -// AppBarAPI.cs: Interfaces with the Windows AppBar API. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.AppBarWindow -{ - using System; - using System.Runtime.InteropServices; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Interop; - - public enum Edge - { - None = -1, - Left = 0, - Top = 1, - Right = 2, - Bottom = 3 - } - - /// - /// The Windows AppBar API - /// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shappbarmessage - /// - internal class AppBarApi - { - private readonly uint callbackMessage; - private readonly WindowMovement windowMovement; - private HwndSource? hwndSource; - private bool positioning; - - public AppBarApi(WindowMovement windowMovement) - { - this.windowMovement = windowMovement; - this.callbackMessage = WinApi.RegisterWindowMessage("MorphicAppBarMessage"); - this.windowMovement.Ready += (sender, args) => - { - // Remove it from alt+tab - // int style = (int) WinApi.GetWindowLong(this.WindowHandle, WinApi.GWL_EXSTYLE); - // style |= WinApi.WS_EX_TOOLWINDOW; - // WinApi.SetWindowLong(this.WindowHandle, WinApi.GWL_EXSTYLE, (IntPtr) style); - }; - } - - private IntPtr WindowHandle => this.windowMovement.WindowHandle; - - /// - /// The edge on which the app bar is. - /// - public Edge Edge { get; private set; } = Edge.None; - - /// - /// Set the window to be at the given edge, and reserves desktop space for it. The window will be - /// moved to the edge and stretched to fit. - /// - /// The edge to dock to. - public void Apply(Edge edge) - { - Edge last = this.Edge; - this.Edge = edge; - - if (this.Edge == Edge.None) - { - this.Remove(); - } - else if (this.Edge == last) - { - this.Update(); - } - else - { - if (last != Edge.None) - { - this.Remove(); - } - - this.Add(); - } - } - - /// - /// Updates the reserved area to match the new size of the window. - /// - public void Update() - { - if (this.Edge != Edge.None) - { - this.SetPos(); - } - } - - /// - /// Adds an app bar to the desktop. - /// - private void Add() - { - WinApi.APPBARDATA appBarData = this.AppBarData(); - appBarData.uCallbackMessage = this.callbackMessage; - AppBarMessage(WinApi.ABMessage.ABM_NEW, ref appBarData); - - if (this.hwndSource is null) - { - this.hwndSource = HwndSource.FromHwnd(this.WindowHandle); - this.hwndSource?.AddHook(this.WindowProc); - } - - this.SetPos(); - } - - /// - /// Set the position of the app bar. - /// - private void SetPos() - { - if (this.positioning) - { - this.positioning = false; - return; - } - this.positioning = true; - - WinApi.APPBARDATA appBarData = this.AppBarData(); - - appBarData.uEdge = (uint) this.Edge; - - Rect windowRect = this.windowMovement.GetWindowRect(); - Rect screen = this.windowMovement.GetScreenSize(); - - // Request the full length of the screen, at the relevant edge. - Rect rect = screen; - if (this.Edge.IsHorizontal()) - { - rect.Height = windowRect.Height; - if (this.Edge == Edge.Bottom) - { - rect.Y = screen.Bottom - windowRect.Height; - } - } - else if (this.Edge.IsVertical()) - { - rect.Width = windowRect.Width; - if (this.Edge == Edge.Right) - { - rect.X = screen.Right - windowRect.Width; - } - } - - // Ask for a suggested rect - Windows will adjust it to be clear of other app bars. - appBarData.rc = rect.ToRECT(); - AppBarMessage(WinApi.ABMessage.ABM_QUERYPOS, ref appBarData); - - // Accept the edge position, ignore the rest. - switch (this.Edge) - { - case Edge.Left: - appBarData.rc.Right = appBarData.rc.Left + (int)rect.Width; - break; - case Edge.Top: - appBarData.rc.Bottom = appBarData.rc.Top + (int)rect.Height; - break; - case Edge.Right: - appBarData.rc.Left = appBarData.rc.Right - (int)rect.Width; - break; - case Edge.Bottom: - appBarData.rc.Top = appBarData.rc.Bottom - (int)rect.Height; - break; - } - - // Move the window. - this.windowMovement.NoMove = true; - this.windowMovement.SetWindowRect(appBarData.rc); - - // Set the app-bar position. - AppBarMessage(WinApi.ABMessage.ABM_SETPOS, ref appBarData); - - Task.Delay(500).ContinueWith(t => - { - this.windowMovement.NoMove = false; - this.windowMovement.SetWindowRect(appBarData.rc); - }); - - this.positioning = false; - } - - /// - /// Removes the app bar. - /// - private void Remove() - { - WinApi.APPBARDATA appBarData = this.AppBarData(); - AppBarMessage(WinApi.ABMessage.ABM_REMOVE, ref appBarData); - } - - private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - if (msg == this.callbackMessage) - { - WinApi.ABNotify abn = (WinApi.ABNotify) wParam; - switch (abn) - { - case WinApi.ABNotify.ABN_POSCHANGED: - this.SetPos(); - break; - case WinApi.ABNotify.ABN_STATECHANGE: - break; - case WinApi.ABNotify.ABN_FULLSCREENAPP: - break; - case WinApi.ABNotify.ABN_WINDOWARRANGE: - break; - } - } - - return IntPtr.Zero; - } - - private WinApi.APPBARDATA AppBarData() - { - WinApi.APPBARDATA appBarData = new WinApi.APPBARDATA(); - appBarData.cbSize = Marshal.SizeOf(appBarData); - appBarData.hWnd = this.WindowHandle; - return appBarData; - } - - private static void AppBarMessage(WinApi.ABMessage message, ref WinApi.APPBARDATA appBarData) - { - WinApi.SHAppBarMessage((int)message, ref appBarData); - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Bar/UI/AppBarWindow/WindowMovement.cs b/Morphic.Client/Bar/UI/AppBarWindow/WindowMovement.cs deleted file mode 100644 index 2d4c49b7..00000000 --- a/Morphic.Client/Bar/UI/AppBarWindow/WindowMovement.cs +++ /dev/null @@ -1,301 +0,0 @@ -// WindowMovement.cs: Low-level window positioning. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.AppBarWindow -{ - using System; - using System.Windows; - using System.Windows.Interop; - - /// - /// Intercepts the low-level Window move/resize messages, so the window rect can be adjusted while it's - /// being moved or resized. - /// - /// All dimensions in and out of this class are measured in device pixels. - /// - public class WindowMovement - { - private readonly Window window; - private bool firstEvent; - private Rect initialRect = Rect.Empty; - private Vector mouseOffset; - private Point mouseStart; - - public WindowMovement(Window window) - { - this.window = window; - - this.window.SourceInitialized += this.WindowOnSourceInitialized; - } - - /// true if the user is currently moving the window. - public bool IsMoving { get; private set; } - - /// Prevent the window from being moved. - public bool NoMove { get; set; } - - /// true to keep the window in place during resizing. - public bool AnchorSizingRect { get; set; } = true; - - public IntPtr WindowHandle { get; private set; } - - /// - /// true to ignore NoMove property. - /// - private bool IgnoreLock { get; set; } - - private WinApi.MONITORINFO MonitorInfo => WinApi.GetMonitorInfo(this.WindowHandle); - - /// Raised while the window is being moved. - public event EventHandler? Moving; - - public event EventHandler? EnterSizeMove; - public event EventHandler? MoveComplete; - public event EventHandler? Ready; - - private void WindowOnSourceInitialized(object? sender, EventArgs e) - { - // Add the hook for the windows messages. - this.WindowHandle = new WindowInteropHelper(this.window).Handle; - HwndSource.FromHwnd(this.WindowHandle)?.AddHook(this.WindowProc); - - this.OnReady(); - } - - private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - IntPtr result = IntPtr.Zero; - - switch (msg) - { - case WinApi.WM_WINDOWPOSCHANGED: - case WinApi.WM_WINDOWPOSCHANGING: - // Prevent the window being moved. - if (!this.IgnoreLock && this.NoMove) - { - WinApi.WINDOWPOS windowPos = WinApi.WINDOWPOS.FromPointer(lParam); - if (this.NoMove) - { - windowPos.flags |= WinApi.SWP_NOMOVE; - } - - windowPos.CopyToPointer(lParam); - } - break; - - case WinApi.WM_ENTERSIZEMOVE: - this.firstEvent = true; - this.OnEnterSizeMove(); - break; - - case WinApi.WM_EXITSIZEMOVE: - if (this.IsMoving) - { - this.OnMoveComplete(); - } - - this.IsMoving = false; - break; - - case WinApi.WM_MOVING: - MovementEventArgs eventArgs = new MovementEventArgs(); - - eventArgs.Cursor = WinApi.GetCursorPos(); - eventArgs.Rect = WinApi.RECT.FromPointer(lParam).ToRect(); - - eventArgs.IsFirst = this.firstEvent; - if (this.firstEvent) - { - this.mouseStart = eventArgs.Cursor; - this.initialRect = eventArgs.Rect; - this.mouseOffset = this.mouseStart - this.initialRect.TopLeft; - } - - eventArgs.InitialRect = this.initialRect; - eventArgs.SupposedRect = this.initialRect; - - if (!this.firstEvent) - { - eventArgs.SupposedRect.X = eventArgs.Cursor.X - this.mouseOffset.X; - eventArgs.SupposedRect.Y = eventArgs.Cursor.Y - this.mouseOffset.Y; - } - - this.IsMoving = true; - - // Call the event handler. - this.OnMoving(eventArgs); - - if (!eventArgs.NewInitialRect.IsEmpty) - { - this.initialRect = eventArgs.NewInitialRect; - if (this.firstEvent) - { - this.mouseOffset = this.mouseStart - this.initialRect.TopLeft; - } - else - { - this.mouseOffset.X = this.initialRect.Width / 2; - this.mouseOffset.Y = this.initialRect.Height / 2; - if (Math.Abs(this.mouseOffset.X) > this.initialRect.Width) - { - } - if (Math.Abs(this.mouseOffset.Y) > this.initialRect.Height) - { - } - } - } - - this.firstEvent = false; - if (eventArgs.Handled) - { - new WinApi.RECT(eventArgs.Rect).CopyToPointer(lParam); - handled = true; - result = new IntPtr(1); - } - - break; - } - - return result; - } - - /// Called when the window is being moved. - /// - protected virtual void OnMoving(MovementEventArgs e) - { - this.Moving?.Invoke(this, e); - } - - protected virtual void OnEnterSizeMove() - { - this.EnterSizeMove?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnMoveComplete() - { - this.MoveComplete?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnReady() - { - this.Ready?.Invoke(this, EventArgs.Empty); - } - - /// - /// Like Window.DragMove, but doesn't fire the Click event. - /// - /// true to leave `NoMove` alone. - public void DragMove(bool respectNoMove = false) - { - if (!this.NoMove || !respectNoMove) - { - this.IgnoreLock = true; - WinApi.DragMove(this.WindowHandle); - this.IgnoreLock = false; - } - } - - /// - /// Gets the work area of the screen the window is (mostly) on. - /// - /// - public Rect GetWorkArea() - { - return this.MonitorInfo.rcWork.ToRect(); - } - - /// - /// Gets the work area at the given point. - /// - /// - /// - public Rect GetWorkArea(Point pt) - { - return WinApi.GetMonitorInfo(pt).rcWork.ToRect(); - } - - /// - /// Get the window rectangle. - /// - /// - internal WinApi.RECT GetWindowRc() - { - WinApi.GetWindowRect(this.WindowHandle, out WinApi.RECT rc); - return rc; - } - - /// - /// Get the window rectangle. - /// - /// - public Rect GetWindowRect() - { - return this.GetWindowRc().ToRect(); - } - - /// - /// Gets the size of the screen, that the window is located on. - /// - /// - public Rect GetScreenSize() - { - return this.MonitorInfo.rcMonitor.ToRect(); - } - - internal void SetWindowRect(WinApi.RECT rc) - { - bool ignored = this.IgnoreLock; - this.IgnoreLock = true; - WinApi.MoveWindow(this.WindowHandle, rc.Left, rc.Top, rc.Right - rc.Left, rc.Bottom - rc.Top, true); - this.IgnoreLock = ignored; - } - - public void SetWindowRect(Rect rect) - { - bool ignored = this.IgnoreLock; - this.IgnoreLock = true; - WinApi.MoveWindow(this.WindowHandle, (int)rect.Left, (int)rect.Top, (int)rect.Width, (int)rect.Height, true); - this.IgnoreLock = ignored; - } - - public static Point GetCursorPos() - { - return WinApi.GetCursorPos(); - } - - public class MovementEventArgs : EventArgs - { - /// The window rect before the resize/move began. - public Rect InitialRect = Rect.Empty; - - /// - /// Set in the event handler to change the initial rect, which used to calculate the SupposedRect property. - /// - public Rect NewInitialRect = Rect.Empty; - - /// The window rect. Update in the event handler to change it. - public Rect Rect; - - /// The window rect that the window should have, if there were no adjustments to Rect. - public Rect SupposedRect = Rect.Empty; - - /// - /// Set in the event handler to true if the event has been handled and the window rect has changed. - /// - public bool Handled { get; set; } - - /// true if this is the first event of the current resize/move loop. - public bool IsFirst { get; set; } - - /// The mouse cursor position on the screen. - public Point Cursor { get; set; } - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Bar/UI/BarControl.cs b/Morphic.Client/Bar/UI/BarControl.cs deleted file mode 100644 index ab178d33..00000000 --- a/Morphic.Client/Bar/UI/BarControl.cs +++ /dev/null @@ -1,278 +0,0 @@ -// BarControl.cs: The control for a bar. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Media; - using BarControls; - using Data; - using Button = System.Windows.Controls.Button; - using Control = System.Windows.Controls.Control; - using HorizontalAlignment = System.Windows.HorizontalAlignment; - using Orientation = System.Windows.Controls.Orientation; - - /// - /// This is the thing that contains bar items. - /// - public class BarControl : WrapPanel, INotifyPropertyChanged - { - public static readonly DependencyProperty ItemSpacingProperty = DependencyProperty.Register("ItemSpacing", typeof(double), typeof(BarControl), new PropertyMetadata(default(double))); - public new static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(BarControl), new PropertyMetadata(default(double))); - public new static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(BarControl), new PropertyMetadata(default(double))); - public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(List), typeof(BarControl), new PropertyMetadata(default(List))); - - public BarControl() - { - this.Bar = new BarData(); - } - - public BarData Bar { get; set; } - public bool IsPrimary { get; set; } - - /// - /// Disable wrapping of the child controls. - /// - public bool NoWrap => this.IsPrimary && this.Bar.Overflow != BarOverflow.Wrap; - - public double Scale { get; set; } - - public void ApplyScale() - { - this.LayoutTransform = new ScaleTransform(this.Scale, this.Scale); - } - - public double ScaledItemWidth => Math.Ceiling(this.ItemWidth * this.Scale); - public double ScaledItemHeight => Math.Ceiling(this.ItemHeight * this.Scale); - - public double ItemSpacing - { - get => (double)this.GetValue(ItemSpacingProperty); - set => this.SetValue(ItemSpacingProperty, value); - } - - public List ItemsSource - { - get => (List)this.GetValue(ItemsSourceProperty); - set => this.SetValue(ItemsSourceProperty, value); - } - - public IEnumerable ItemControls => this.Children.OfType(); - - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Raised when tabbing through items has gone beyond the last item. - /// - public event EventHandler? EndTab; - - private Size GetChildSize(UIElement child) - { - return this.GetChildSize(child.DesiredSize); - } - - private Size GetChildSize(Size desiredSize) - { - return new Size( - double.IsNaN(this.ItemWidth) ? desiredSize.Width : this.ItemWidth, - double.IsNaN(this.ItemHeight) ? desiredSize.Height : this.ItemHeight - ); - } - - /// - /// Create the controls for the items. - /// - protected override UIElementCollection CreateUIElementCollection(FrameworkElement logicalParent) - { - UIElementCollection uiElementCollection = base.CreateUIElementCollection(logicalParent); - foreach (BarItem item in this.ItemsSource) - { - uiElementCollection.Add(this.CreateItem(item)); - } - - if (Orientation == Orientation.Horizontal && uiElementCollection.Count > 0) - { - var barItemControl = (BarItemControl)uiElementCollection[uiElementCollection.Count - 1]; - var margin = barItemControl.Margin; - barItemControl.Margin = new Thickness(margin.Left, margin.Top, 0, margin.Bottom); - } - - uiElementCollection.Add(this.CreateEndTabControl()); - return uiElementCollection; - } - - protected override Size MeasureOverride(Size constraint) - { - return this.MeasureArrange(constraint, true); - } - - protected override Size ArrangeOverride(Size finalSize) - { - return this.MeasureArrange(finalSize, false); - } - - /// - /// Measure or arrange the child items - slightly different to the default implementation, where it allows - /// wrapping to be turned off. - /// - /// The suggested size. - /// true to only measure, otherwise arrange then items. - /// Override the controls orientation. - /// A size that fits the arranged items. - public Size MeasureArrange(Size finalSize, bool measure, Orientation? orientationOverride = null) - { - Orientation orientation = orientationOverride ?? this.Orientation; - - double rowHeight = 0; - - CorrectedCoords pos = new CorrectedCoords(0, 0, orientation); - CorrectedCoords size = new CorrectedCoords(finalSize, orientation); - CorrectedCoords actualSize = new CorrectedCoords(0, 0, orientation); - - List children = this.InternalChildren.OfType().Where(c => c is not null).ToList(); - if (!children.Any()) - { - return finalSize; - } - - if (measure) - { - Size childAvailableSize = this.GetChildSize(finalSize); - children.ForEach(c => c.Measure(childAvailableSize)); - } - - // Get the widest - double widest = children.Select(c => new CorrectedCoords(c.DesiredSize, orientation).Width).Max(); - - size.Width = Math.Max(size.Width, widest); - - // first item of the row - bool firstItem = true; - - foreach (BarItemControl child in children) - { - CorrectedCoords childSize = new CorrectedCoords(this.GetChildSize(child), orientation); - - if (pos.X + childSize.Width >= size.Width) - { - if (!this.NoWrap) - { - // new row - firstItem = true; - pos.X = 0; - pos.Y += rowHeight + this.ItemSpacing; - - rowHeight = 0; - } - } - - if (!firstItem) - { - pos.X += this.ItemSpacing; - } - - if (!measure) - { - child.Arrange(new Rect(pos.ToPoint(), childSize.ToSize())); - } - - rowHeight = Math.Max(rowHeight, childSize.Height); - pos.X += childSize.Width; - - actualSize.Width = Math.Max(actualSize.Width, pos.X); - actualSize.Height = pos.Y + rowHeight; - - firstItem = false; - } - - actualSize.Width = Math.Ceiling(actualSize.Width); - actualSize.Height = Math.Ceiling(actualSize.Height); - - // if this is a horizontal bar, re-arrange any ButtonBarControl children to be vertically centered - // NOTE: we should redesign/refactor our layout logic to handle all button types equally (so we don't have to special-case specific controls...nor do layout twice) - if ((orientation == Orientation.Horizontal) && (measure == false)) - { - foreach (BarItemControl child in children) - { - if (child is ButtonBarControl) { - CorrectedCoords childSize = new CorrectedCoords(this.GetChildSize(child), orientation); - var offset = VisualTreeHelper.GetOffset(child); - var verticallyCenteredPosition = new Point(x: offset.X, y: offset.Y + ((actualSize.Y - childSize.Height) / 2)); - child.Arrange(new Rect(verticallyCenteredPosition, childSize.ToSize())); - } - } - } - - return actualSize.ToSize(); - } - - public Size GetSize(Size size, Orientation? orientationOverride = null) - { - Size newSize = this.MeasureOverride(size); - // this.MeasureArrange(size, true, orientationOverride); - newSize.Width *= this.Scale; - newSize.Height *= this.Scale; - return newSize; - } - - /// - /// Add a bar item to the control. - /// - /// - /// The bar item control. - public BarItemControl CreateItem(BarItem item) - { - BarItemControl control = BarItemControl.FromItem(item); - control.Orientation = this.Orientation; - control.Style = new Style(control.GetType(), this.Resources["BarItemStyle"] as Style); - return control; - } - - /// - /// Add a secret control as the last item. This is to move focus to the other bar. - /// - private Control CreateEndTabControl() - { - Button tb = new Button() - { - Focusable = true, - IsTabStop = true, - TabIndex = int.MaxValue, - Width = 10, - Height = 10, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Bottom - }; - - tb.GotFocus += (o, a) => - { - this.OnEndTab(); - }; - - return tb; - } - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - protected virtual void OnEndTab() - { - this.EndTab?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/Morphic.Client/Bar/UI/BarControls/BarContextMenu.cs b/Morphic.Client/Bar/UI/BarControls/BarContextMenu.cs deleted file mode 100644 index d3c292b6..00000000 --- a/Morphic.Client/Bar/UI/BarControls/BarContextMenu.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Morphic.Client.Bar.UI.BarControls -{ - using System.Collections.Generic; - using System.Windows.Controls; - using Menu; - - public class BarContextMenu - { - - private static readonly string SettingsFormat = "ms-settings:{0}"; - private static readonly string DemoFormat = "https://morphic.org/rd/{0}-vid"; - private static readonly string LearnMoreFormat = "https://morphic.org/rd/{0}"; - - public static ContextMenu? CreateContextMenu(Dictionary items, string? telemetryCategory = null) - { - ContextMenu menu = new ContextMenu(); - - foreach ((string? name, string? target) in items) - { - string? format, finalName; - MorphicMenuItem.MorphicMenuItemTelemetryType? telemetryType; - switch (name) - { - case "learn": - format = BarContextMenu.LearnMoreFormat; - finalName = "_Learn more"; - telemetryType = MorphicMenuItem.MorphicMenuItemTelemetryType.LearnMore; - break; - case "demo": - format = BarContextMenu.DemoFormat; - finalName = "Quick _Demo video"; - telemetryType = MorphicMenuItem.MorphicMenuItemTelemetryType.QuickDemoVideo; - break; - case "settings": - case "setting": - format = BarContextMenu.SettingsFormat; - finalName = "_Settings"; - telemetryType = MorphicMenuItem.MorphicMenuItemTelemetryType.Settings; - break; - default: - format = null; - finalName = name; - telemetryType = null; - break; - } - - string finalTarget = (format is null) - ? target - : string.Format(format, target); - - MorphicMenuItem item = new MorphicMenuItem() - { - Header = finalName, - Open = finalTarget, - ParentMenuType = MorphicMenuItem.MenuType.contextMenu, - TelemetryType = telemetryType, - TelemetryCategory = (telemetryType is not null) ? telemetryCategory : null - }; - - menu.Items.Add(item); - } - - return menu.Items.Count > 0 - ? menu - : null; - } - } -} diff --git a/Morphic.Client/Bar/UI/BarControls/BarItemControl.cs b/Morphic.Client/Bar/UI/BarControls/BarItemControl.cs deleted file mode 100644 index 5784fb01..00000000 --- a/Morphic.Client/Bar/UI/BarControls/BarItemControl.cs +++ /dev/null @@ -1,208 +0,0 @@ -// BarItemControl.cs: Base control for bar items. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.BarControls -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Controls.Primitives; - using System.Windows.Media; - using Data; - - /// - /// A bar item control. - /// - public class BarItemControl : ContentControl, INotifyPropertyChanged - { - private BarItemSize maxItemSize = BarItemSize.Large; - - /// - /// The bar item represented by this control. - /// - public BarItem BarItem { get; } - - /// - /// The bar that contains the bar item. - /// - public BarData Bar => this.BarItem.Bar; - - /// Tool tip header. - public string? ToolTipHeader => this.BarItem.ToolTipHeader; - - /// Tool tip text. - public string? ToolTipText => this.BarItem.ToolTip; - - protected ThemeHandler ThemeHandler { get; } - - /// - /// The maximum size of an item. - /// - public BarItemSize MaxItemSize - { - get => this.maxItemSize; - set - { - this.maxItemSize = value; - this.OnPropertyChanged(); - this.OnPropertyChanged(nameof(this.ItemSize)); - } - } - - /// - /// The size of this item. - /// - public BarItemSize ItemSize => (BarItemSize)Math.Min((int)this.BarItem.Size, (int)this.maxItemSize); - - /// true if the last focus was performed by the keyboard. - public bool FocusedByKeyboard { get; set; } - - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", - typeof(Orientation), typeof(MultiButtonBarControl), new PropertyMetadata(default(Orientation), (o, args) => - { - if (o is BarItemControl control) - { - control.OnOrientationChanged(); - } - })); - - public static readonly DependencyProperty ActiveThemeProperty = DependencyProperty.Register("ActiveTheme", typeof(Theme), typeof(ButtonBarControl), new PropertyMetadata(default(Theme))); - - public Orientation Orientation - { - get => (Orientation)this.GetValue(OrientationProperty); - set => this.SetValue(OrientationProperty, value); - } - - public event EventHandler? OrientationChanged; - - /// - /// Create an instance of this class, using the given bar item. - /// - /// The bar item that this control displays. - public BarItemControl(BarItem barItem) - { - this.DataContext = this; - this.BarItem = barItem; - this.ThemeHandler = new ThemeHandler(this, this.BarItem.Theme); - - this.ToolTip = $"{this.ToolTipHeader}|{this.ToolTipText}"; - - this.Loaded += this.OnLoaded; - this.MouseRightButtonUp += (sender, args) => - { - args.Handled = this.OpenContextMenu(sender); - }; - - this.ThemeHandler.ThemeStateChanged += (sender, args) => - { - this.ActiveTheme = args.ActiveTheme; - }; - this.ActiveTheme = this.BarItem.Theme; - } - - /// - /// Open a context menu. - /// - /// - /// - /// - protected bool OpenContextMenu(object? control, Dictionary? items = null, string? telemetryCategory = null) - { - ContextMenu? menu = BarContextMenu.CreateContextMenu(items ?? this.BarItem.Menu, telemetryCategory ?? this.BarItem.TelemetryCategory); - if (menu is not null) - { - menu.Placement = PlacementMode.Top; - menu.PlacementTarget = control as UIElement ?? this; - menu.IsOpen = true; - return true; - } - - return false; - } - - private void OnLoaded(object sender, RoutedEventArgs args) - { - // Override the apparent behaviour of ContentControl elements, where they make the control focusable. - foreach (UIElement elem in this.GetAllChildren().OfType()) - { - elem.Focusable = elem.Focusable && elem is Button || elem == this; - } - - foreach (BitmapOrXamlImage bitmapOrXamlImage in this.GetAllChildren().OfType()) - { - bitmapOrXamlImage.ImageSourceChanged += BitmapOrXamlImage_ImageSourceChanged; - this.BitmapOrXamlImage_ImageSourceChanged(bitmapOrXamlImage, new PropertyChangedEventArgs("ImageSource")); - } - } - - private void BitmapOrXamlImage_ImageSourceChanged(object sender, PropertyChangedEventArgs e) - { - if (sender is BitmapOrXamlImage image) - { - if (image.ImageSource is DrawingImage drawingImage) - { - this.DrawingBrush = - BarImages.ChangeDrawingColor(drawingImage.Drawing, this.BarItem.Color, this.DrawingBrush); - } - } - } - - public IEnumerable GetAllChildren(DependencyObject? parent = null) - { - return LogicalTreeHelper.GetChildren(parent ?? this) - .OfType() - .SelectMany(this.GetAllChildren) - .Append(parent ?? this); - } - - /// - /// Creates a control for the given bar item. - /// - /// - /// The control for the item, the type depends on the item. - public static BarItemControl FromItem(BarItem item) - { - return (Activator.CreateInstance(item.ControlType, item) as BarItemControl)!; - } - - /// - /// The brush used for monochrome images. - /// - public SolidColorBrush? DrawingBrush { get; protected set; } - - public Theme ActiveTheme - { - get => (Theme)this.GetValue(ActiveThemeProperty); - set => this.SetValue(ActiveThemeProperty, value); - } - - #region INotifyPropertyChanged - - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - #endregion - - protected virtual void OnOrientationChanged() - { - this.OnPropertyChanged(nameof(this.Orientation)); - this.OrientationChanged?.Invoke(this, EventArgs.Empty); - } - } -} diff --git a/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml b/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml deleted file mode 100644 index d07c4fdd..00000000 --- a/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml.cs b/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml.cs deleted file mode 100644 index aecbaf76..00000000 --- a/Morphic.Client/Bar/UI/BarControls/BitmapOrXamlImage.xaml.cs +++ /dev/null @@ -1,113 +0,0 @@ -// BarMultiButtonControl.xaml.cs: Control for Bar images. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; - -namespace Morphic.Client.Bar.UI.BarControls -{ - /// - /// Interaction logic for MorphicImage.xaml - /// - public partial class BitmapOrXamlImage : UserControl - { - public BitmapOrXamlImage() - { - InitializeComponent(); - } - - public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(BitmapOrXamlImage), new PropertyMetadata(null, new PropertyChangedCallback(OnImageSourceChanged))); - public ImageSource? ImageSource - { - get => (ImageSource)GetValue(ImageSourceProperty); - set - { - SetValue(ImageSourceProperty, value); - } - } - // - private static void OnImageSourceChanged(object sender, DependencyPropertyChangedEventArgs e) - { - var senderAsBitmapOrXamlImage = ((BitmapOrXamlImage)sender); - senderAsBitmapOrXamlImage.UpdateCurrentContent(); - if (senderAsBitmapOrXamlImage.ImageSourceChanged is not null) - { - senderAsBitmapOrXamlImage.ImageSourceChanged(sender, new PropertyChangedEventArgs("ImageSource")); - } - } - // - public event PropertyChangedEventHandler ImageSourceChanged; - - public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(BitmapOrXamlImage), new PropertyMetadata(Stretch.Uniform, new PropertyChangedCallback(OnStretchChanged))); - public Stretch Stretch - { - get => (Stretch)GetValue(StretchProperty); - set - { - SetValue(StretchProperty, value); - } - } - // - private static void OnStretchChanged(object sender, DependencyPropertyChangedEventArgs e) - { - ((BitmapOrXamlImage)sender).UpdateCurrentContent(); - } - - public static readonly DependencyProperty XamlContentProperty = DependencyProperty.Register("XamlContent", typeof(Canvas), typeof(BitmapOrXamlImage), new PropertyMetadata(null, new PropertyChangedCallback(OnXamlContentChanged))); - public Canvas? XamlContent - { - get => (Canvas)GetValue(XamlContentProperty); - set - { - SetValue(XamlContentProperty, value); - } - } - // - private static void OnXamlContentChanged(object sender, DependencyPropertyChangedEventArgs e) - { - ((BitmapOrXamlImage)sender).UpdateCurrentContent(); - } - - private void UpdateCurrentContent() - { - var imageSource = (ImageSource)GetValue(ImageSourceProperty); - var xamlContent = (Canvas)GetValue(XamlContentProperty); - var stretch = (Stretch)GetValue(StretchProperty); - - if (imageSource is not null) - { - this.ViewboxContent.Content = new Image() - { - Source = imageSource, - Stretch = stretch - }; - } - else if (xamlContent is not null) - { - this.ViewboxContent.Content = xamlContent; - } - else - { - this.ViewboxContent.Content = null; - } - } - } -} diff --git a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml b/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml deleted file mode 100644 index e359476d..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml.cs b/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml.cs deleted file mode 100644 index 4d91f7bb..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml.cs +++ /dev/null @@ -1,70 +0,0 @@ -// BarButtonControl.xaml.cs: Control for Bar buttons. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.BarControls -{ - using System.ComponentModel; - using System.Windows; - using Data; - - /// - /// The control for Button bar items. - /// - public partial class ButtonBarControl : BarItemControl - { - public ButtonBarControl(BarButton barItem) : base(barItem) - { - this.PropertyChanged += this.OnPropertyChanged; - this.InitializeComponent(); - this.BarItem.PropertyChanged += (sender, args) => - { - this.OnPropertyChanged(nameof(this.ButtonResource)); - }; - } - - public new BarButton BarItem => (BarButton) base.BarItem; - - /// - /// The control in the resource library to use for the specific size of button. - /// - public object ButtonResource - { - get - { - BarItemSize size = this.ItemSize; - if ((this.BarItem.ImageSource is null) && (this.BarItem.XamlContent is null)) - { - size = BarItemSize.TextOnly; - } - string resourceName = size + "Button"; - object resource = this.Resources[resourceName]; - return resource; - } - } - - private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - // Update the button resource when the item size changes. - if (e.PropertyName == nameof(this.ItemSize)) - { - this.OnPropertyChanged(nameof(this.ButtonResource)); - } - } - - private async void Button_OnClick(object sender, RoutedEventArgs e) - { - if (this.BarItem.Action is not null) - { - await this.BarItem.Action.InvokeAsync(); - } - } - } - -} diff --git a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml b/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml deleted file mode 100644 index 3baaa17c..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - diff --git a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml.cs b/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml.cs deleted file mode 100644 index f88e5fba..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml.cs +++ /dev/null @@ -1,52 +0,0 @@ -// BarImageControl.xaml.cs: Control for Bar images. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.BarControls -{ - using System.Windows; - using Data; - using Morphic.Client.Bar.Data.Actions; - - /// - /// The control for Button bar items. - /// - public partial class ImageBarControl : BarItemControl - { - public ImageBarControl(BarImage barItem) : base(barItem) - { - this.InitializeComponent(); - } - - public new BarButton BarItem => (BarButton) base.BarItem; - - private async void Button_OnClick(object sender, RoutedEventArgs e) - { - if (this.BarItem.Action is not null) - { - await this.BarItem.Action.InvokeAsync(); - } - } - - private async void Button_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) - { - if (this.BarItem.Action is not null) - { - if (this.BarItem.Action is InternalAction) - { - if (((InternalAction)this.BarItem.Action).FunctionOnRightClickAlso == true) - { - await this.BarItem.Action.InvokeAsync(); - } - } - } - } - } - -} diff --git a/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml b/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml deleted file mode 100644 index 95f21b80..00000000 --- a/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml.cs b/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml.cs deleted file mode 100644 index 6075da31..00000000 --- a/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml.cs +++ /dev/null @@ -1,607 +0,0 @@ -// BarMultiButtonControl.xaml.cs: Control for Bar images. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client.Bar.UI.BarControls -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.Linq; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Automation.Peers; - using System.Windows.Automation.Provider; - using System.Windows.Controls; - using System.Windows.Controls.Primitives; - using System.Windows.Input; - using System.Windows.Media; - using Data; - using Data.Actions; - using Settings.SettingsHandlers; - - /// - /// The control for Button bar items. - /// - public partial class MultiButtonBarControl : BarItemControl - { - public MultiButtonBarControl(BarMultiButton barItem) : base(barItem) - { - this.Buttons = this.BarItem.Buttons.Values.Select(b => new ButtonWrapper(this, b)).ToList(); - - this.InitializeComponent(); - - // Apply theming to the dynamic buttons when they're created. - this.ButtonContainer.ItemContainerGenerator.StatusChanged += async (sender, args) => - { - if (this.ButtonContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) - { - foreach (ButtonWrapper b in this.ButtonContainer.ItemContainerGenerator.Items) - { - ContentPresenter content = (ContentPresenter)this.ButtonContainer.ItemContainerGenerator.ContainerFromItem(b); - content.ApplyTemplate(); - - if (content.ContentTemplate.FindName("ControlButton", content) is ButtonBase control) - { - await b.SetControlAsync(control); - } - } - } - }; - - // Set the navigation modes, depending on its type. - bool isPair = (this.BarItem.Type == MultiButtonType.Toggle - || this.BarItem.Type == MultiButtonType.Additive); - -#if CombinePairs - // For keyboard navigation, paired buttons act as a single control - this.Focusable = isPair; - this.Panel.SetValue(FocusManager.IsFocusScopeProperty, isPair); - this.Panel.SetValue(KeyboardNavigation.DirectionalNavigationProperty, - isPair ? KeyboardNavigationMode.None : KeyboardNavigationMode.Continue); - this.Panel.SetValue(KeyboardNavigation.TabNavigationProperty, - isPair ? KeyboardNavigationMode.None : KeyboardNavigationMode.Continue); -#endif - - if (isPair) - { - this.KeyDown += this.OnKeyDown_ButtonPair; - } - } - - /// - /// Activate one of the buttons of a pair, if -/+ is pressed. - /// - /// - /// - private async void OnKeyDown_ButtonPair(object sender, KeyEventArgs e) - { - int clickIndex = -1; - - switch (e.Key) - { - case Key.Subtract: - case Key.OemMinus: - clickIndex = 0; - break; - case Key.Add: - case Key.OemPlus: - clickIndex = 1; - break; - } - - if (clickIndex > -1) - { - if (this.Buttons[clickIndex].Control is ButtonBase button) - { - // // Make it look like it's being clicked. - // this.ControlTheme[button].IsMouseDown = true; - // this.UpdateTheme(button); - - // Click it - FrameworkElementAutomationPeer automationPeer; - if (button is ToggleButton toggle) - { - automationPeer = new ToggleButtonAutomationPeer(toggle); - } - else - { - automationPeer = new ButtonAutomationPeer((Button)button); - } - ((IInvokeProvider?)automationPeer.GetPattern(PatternInterface.Invoke))?.Invoke(); - - // await Task.Delay(250); - // this.ControlTheme[button].IsMouseDown = false; - // this.UpdateTheme(button); - } - } - } - - public new BarMultiButton BarItem => (BarMultiButton) base.BarItem; - - public List Buttons { get; set; } - - private void Button_Checked(object sender, RoutedEventArgs routedEventArgs) - { - if (sender is ToggleButton button) - { - BarMultiButton.ButtonInfo? buttonInfo = - this.Buttons.Where(b => b.Control == button) - .Select(b => b.Button) - .FirstOrDefault(); - - if (buttonInfo is not null && !buttonInfo.Toggle) - { - button.IsChecked = false; - routedEventArgs.Handled = true; - } - } - } - private void Button_OnRightClick(object sender, MouseEventArgs e) - { - BarMultiButton.ButtonInfo? buttonInfo = - this.Buttons.Where(b => b.Control == sender) - .Select(b => b.Button) - .FirstOrDefault(); - - if (buttonInfo is not null) - { - e.Handled = this.OpenContextMenu(sender, buttonInfo.Menu, buttonInfo.TelemetryCategory); - } - } - - private async void Button_OnClick(object sender, RoutedEventArgs e) - { - BarMultiButton.ButtonInfo? buttonInfo = - this.Buttons.Where(b => b.Control == sender) - .Select(b => b.Button) - .FirstOrDefault(); - - if (buttonInfo is not null) - { - MultiButtonBarControl.LastClickedControl = sender as Control; - MultiButtonBarControl.LastClickedTime = DateTime.Now; - - // Call the button action. - bool? state = (sender as ToggleButton)?.IsChecked; - if (buttonInfo.Action is not null) - { - var result = await buttonInfo.Action.InvokeAsync(buttonInfo.Value, state); - - // if we get an error, deal with that error based on the type of button that was pressed - // NOTE: this is, unfortunately, a temporary solution; this code should be reworked to centrally handle these sorts of things - // (and also to not TOGGLE the state of the button automatically via the GUI (so that it's not toggled unless the operation is successful) - if (result.IsError == true) - { - if (buttonInfo.Value.ToLowerInvariant() == "basicwordribbon") - { - var isBasicSimplifyRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsBasicSimplifyRibbonEnabled(); - // NOTE: we do not handle the error condition (of not being able to capture this value); in the future, we may want to consider showing an error to the user and/or assuming a default toggle state - if (isBasicSimplifyRibbonEnabledResult.IsSuccess == true) - { - ((ToggleButton)sender).IsChecked = isBasicSimplifyRibbonEnabledResult.Value!; - } - } - else if (buttonInfo.Value.ToLowerInvariant() == "essentialswordribbon") - { - var isEssentialsSimplifyRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsEssentialsSimplifyRibbonEnabled(); - // NOTE: we do not handle the error condition (of not being able to capture this value); in the future, we may want to consider showing an error to the user and/or assuming a default toggle state - if (isEssentialsSimplifyRibbonEnabledResult.IsSuccess == true) - { - ((ToggleButton)sender).IsChecked = isEssentialsSimplifyRibbonEnabledResult.Value!; - } - } - } - } - } - } - - /// When a button was last clicked. - public static DateTime? LastClickedTime { get; private set; } - /// The button that was last clicked. - public static Control? LastClickedControl { get; private set; } - - /// - /// Gets the text or icon to be displayed, based on the given text. This allows symbols to be easily expressed - /// in the json. - /// May also return a string prefixed with "icon:", where the rest of the text is the bar icon. - /// - /// - /// - protected string GetDisplayText(string text) - { - var (finalText, icon) = text switch - { - "+" => ("\u2795", "plus"), - "-" => ("\u2796", "minus"), - "||" => ("\u258e \u258e", ""), - "|>" => ("\u25b6", ""), - "[]" => ("\u25a0", ""), - _ => (text, null) - }; - - string? iconPath = string.IsNullOrEmpty(icon) - ? null - : BarImages.GetBarIconFile(icon); - - return iconPath is not null - ? $"icon:{iconPath}" - : finalText; - } - - /// - /// Wraps a button with its theming info. - /// - public class ButtonWrapper : INotifyPropertyChanged - { - public BarMultiButton.ButtonInfo Button { get; set; } - public BarData Bar => this.Button.BarItem.Bar; - - public Theme ActiveTheme { get; private set; } - - public Control? Control { get; set; } - - public string? Text { get; set; } - public ImageSource? ImageSource { get; set; } - public ThemeHandler? ThemeHandler { get; set; } - - private readonly MultiButtonBarControl itemControl; - - public ButtonWrapper(MultiButtonBarControl itemControl, BarMultiButton.ButtonInfo buttonInfo) - { - this.itemControl = itemControl; - this.Button = buttonInfo; - this.ActiveTheme = buttonInfo.BarItem.ControlTheme; - string text = itemControl.GetDisplayText(this.Button.Text); - SolidColorBrush? imageBrush = null; - if (text.StartsWith("icon:")) - { - string icon = text.Substring(text.IndexOf(':') + 1); - this.ImageSource = BarImages.CreateImageSource(icon); - if (this.ImageSource is DrawingImage di) - { - imageBrush = BarImages.ChangeDrawingColor(di.Drawing, this.ActiveTheme.TextColor ?? Colors.White); - } - } - else - { - this.Text = text; - } - - // Update the theme when the property changes. - this.itemControl.PropertyChanged += (sender, args) => - { - if (args.PropertyName == nameof(this.itemControl.ActiveTheme)) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(args.PropertyName)); - if (imageBrush is not null && this.ActiveTheme.TextColor.HasValue) - { - imageBrush.Color = this.ActiveTheme.TextColor.Value; - } - } - }; - } - - public event PropertyChangedEventHandler? PropertyChanged; - - public async Task SetControlAsync(ButtonBase control) - { - this.Control = control; - this.ThemeHandler = new ThemeHandler(this.Control, this.Button.BarItem.ControlTheme); - this.ActiveTheme = this.ThemeHandler.ActiveTheme; - this.ThemeHandler.ThemeStateChanged += (sender, args) => - { - this.ActiveTheme = args.ActiveTheme; - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.ActiveTheme))); - }; - - // For settings controls, get the current value and listen for a change. - if (this.Button.Toggle && this.Button.Action is SettingAction settingAction) - { - Setting setting = settingAction.Solutions.GetSetting(this.Button.Id); - // - var settingValue = await setting.GetValue(); - ((ToggleButton)this.Control).IsChecked = settingValue; - // - setting.Changed += this.SettingOnChanged; - this.Control.Unloaded += (sender, args) => setting.Changed -= this.SettingOnChanged; - } - - // For settings controls with internal actions, get the current value and listen for a change. - if (this.Button.Toggle && this.Button.Action is InternalAction internalAction) - { - // NOTE: these buttons are special-cased; we should ideally create logic which wraps this functionality and is referenced via the JSON files (so that these are not hard-coded) - switch (internalAction.FunctionName) - { - case "darkMode": - bool systemThemeIsLightTheme; - - var getDarkModeStateResult = await Morphic.Client.Bar.Data.Actions.Functions.GetDarkModeStateAsync(); - if (getDarkModeStateResult.IsError == true) - { - Debug.Assert(false, "Could not get dark mode state"); - break; - } - var darkModeState = getDarkModeStateResult.Value!; - - var osVersion = Morphic.Windows.Native.OsVersion.OsVersion.GetWindowsVersion(); - if (osVersion == Windows.Native.OsVersion.WindowsVersion.Win10_v1809) - { - // Windows 10 v1809 - - // NOTE: this is hard-coded, as a patch, because the solutions registry does not yet understand how to capture/apply settings across incompatible handlers - // [and trying to call the Windows 10 v1903+ handlers for apps/system "light theme" will result in a memory access exception under v1809] - // [also: only "AppsUseLightTheme" (and not "SystemUsesLightTheme") existed properly under Windows 10 v1809] - - var openPersonalizeKeyResult = Morphic.Windows.Native.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize", true); - if (openPersonalizeKeyResult.IsError == true) - { - Debug.Assert(false, "Could not get Personalize key from registry (so we cannot watch the dark mode state)"); - } - else - { - var personalizeKey = openPersonalizeKeyResult.Value!; - - var registerForChangesEvent = new Morphic.Windows.Native.Registry.RegistryKey.RegistryKeyChangedEvent(async (sender, e) => - { - var getDarkModeStateResult = await Morphic.Client.Bar.Data.Actions.Functions.GetDarkModeStateAsync(); - if (getDarkModeStateResult.IsError == true) - { - Debug.Assert(false, "Could not get dark mode state"); - return; - } - var darkModeState = getDarkModeStateResult.Value!; - - Setting appsThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(Settings.SolutionsRegistry.SettingId.LightThemeApps); - Application.Current.Dispatcher.Invoke(new Action(() => { - this.InverseSettingOnChanged(sender, new SettingEventArgs(appsThemeSetting, !darkModeState)); - })); - }); - personalizeKey.RegisterForValueChangeNotification(registerForChangesEvent); - - this.Control.Unloaded += (sender, SettingEventArgs) => - { - // dispose of the RegistryKey; this should terminate the notification registration as well - personalizeKey.Dispose(); - }; - } - - // TODO: add a registry hook which watches for the value to change (and which calls "this.InverseSettingOnChanged") - // [and unwire the handler when our control is unloaded] - // TODO: we need to make sure that we wire this up in a way that the registry key doesn't get GC'd prematurely - //appsThemeRegistryKey.Changed += this.InverseSettingOnChanged; - //// - //this.Control.Unloaded += (sender, args) => appsThemeRegistryKey.Changed -= this.InverseSettingOnChanged; - } - else if (osVersion is null) - { - // error - //break; - } - else - { - // Windows 10 v1903+ - - // capture changes to system dark theme (triggering our this.InverseSettingOnChanged event handler) - Setting systemThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(Settings.SolutionsRegistry.SettingId.LightThemeSystem); - systemThemeSetting.Changed += this.InverseSettingOnChanged; - // - this.Control.Unloaded += (sender, args) => systemThemeSetting.Changed -= this.InverseSettingOnChanged; - - // capture changes to apps dark theme (triggering our this.InverseSettingOnChanged event handler) - Setting appsThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(Settings.SolutionsRegistry.SettingId.LightThemeApps); - appsThemeSetting.Changed += this.InverseSettingOnChanged; - // - this.Control.Unloaded += (sender, args) => appsThemeSetting.Changed -= this.InverseSettingOnChanged; - } - - ((ToggleButton)this.Control).IsChecked = darkModeState; - break; - case "volumeMute": - var getMuteStateResult = Morphic.Client.Bar.Data.Actions.Functions.GetMuteState(); - if (getMuteStateResult.IsError == true) - { - Debug.Assert(false, "Could not get volume mute state"); - break; - } - var volumeMuteState = getMuteStateResult.Value!; - - // NOTE: this.Control.Unloaded should capture 'defaultAudioEndpoint' so we don't need to create a class-level reference to it here - var defaultAudioEndpoint = Morphic.Windows.Native.Audio.AudioEndpoint.GetDefaultAudioOutputEndpoint(); - defaultAudioEndpoint.MasterMuteStateChangedEvent += this.MasterMuteStateOnChanged; - // - this.Control.Unloaded += (sender, args) => defaultAudioEndpoint.MasterMuteStateChangedEvent -= this.MasterMuteStateOnChanged; - - ((ToggleButton)this.Control).IsChecked = volumeMuteState; - - break; - case "basicWordRibbon": - var isBasicWordRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsBasicSimplifyRibbonEnabled(); - var basicSimpifyRibbonIsEnabled = isBasicWordRibbonEnabledResult.IsSuccess ? isBasicWordRibbonEnabledResult.Value! : false; - - ((ToggleButton)this.Control).IsChecked = basicSimpifyRibbonIsEnabled; - - break; - case "essentialsWordRibbon": - var isEssentialsWordRibbonEnabledResult = Morphic.Integrations.Office.WordRibbon.IsEssentialsSimplifyRibbonEnabled(); - var essentialsSimpifyRibbonIsEnabled = isEssentialsWordRibbonEnabledResult.IsSuccess ? isEssentialsWordRibbonEnabledResult.Value! : false; - - ((ToggleButton)this.Control).IsChecked = essentialsSimpifyRibbonIsEnabled; - break; - default: - // unknown internal action - break; - } - } - } - - private void SettingOnChanged(object? sender, SettingEventArgs e) - { - if (this.Control is ToggleButton button) - { - button.IsChecked = e.NewValue as bool? ?? false; - } - } - - private void InverseSettingOnChanged(object? sender, SettingEventArgs e) - { - if (this.Control is ToggleButton button) - { - button.IsChecked = !(e.NewValue as bool? ?? false); - } - } - - private void MasterMuteStateOnChanged(object? sender, Morphic.Windows.Native.Audio.AudioEndpoint.MasterMuteStateChangedEventArgs e) - { - Application.Current.Dispatcher.Invoke(new Action(() => - { - if (this.Control is ToggleButton button) - { - button.IsChecked = e.MuteState; - } - })); - } - } - } - - /// - /// Panel for the buttons of a . Child items can either have the same size (of - /// the widest), or their own size. - /// - public class MultiButtonPanel : WrapPanel - { - public static readonly DependencyProperty VariableWidthProperty = DependencyProperty.Register("VariableWidth", typeof(bool), typeof(MultiButtonPanel), new PropertyMetadata(default(bool))); - - public static readonly DependencyProperty ParentOrientationProperty = - DependencyProperty.Register("ParentOrientation", typeof(Orientation), typeof(MultiButtonPanel), - new PropertyMetadata(default(Orientation), (o, args) => ((MultiButtonPanel)o).OnOrientationChanged())); - - public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(double), typeof(MultiButtonPanel), new PropertyMetadata(default(double))); - - public Orientation ParentOrientation - { - get => (Orientation)this.GetValue(ParentOrientationProperty); - set => this.SetValue(ParentOrientationProperty, value); - } - - public bool VariableWidth - { - get => (bool)this.GetValue(VariableWidthProperty); - set => this.SetValue(VariableWidthProperty, value); - } - - public double CornerRadius - { - get => (double)this.GetValue(CornerRadiusProperty); - set => this.SetValue(CornerRadiusProperty, value); - } - - protected void OnOrientationChanged() - { - } - - protected override Size MeasureOverride(Size availableSize) - { - if (!this.VariableWidth || this.ParentOrientation == Orientation.Vertical) - { - // Make all child items the width of the largest. - this.ItemWidth = this.Children.OfType().Select(c => - { - c.Measure(availableSize); - return c.DesiredSize.Width; - }).Max(); - } - else - { - this.ItemWidth = double.NaN; - } - - Size size = base.MeasureOverride(availableSize); - return size; - } - - protected override Size ArrangeOverride(Size finalSize) - { - Size arrangeOverride = base.ArrangeOverride(finalSize); - this.ApplyButtonBorders(); - return arrangeOverride; - } - - /// - /// Applies the rounded corners to the buttons, according to where they are positioned. - /// - private void ApplyButtonBorders() - { - if (this.CornerRadius > 0) - { - // Get the positions of the buttons. - Dictionary positions = this.InternalChildren.OfType() - .Where(c => c.Content is MultiButtonBarControl.ButtonWrapper) - .ToDictionary(c => c, c => c.TranslatePoint(default, this)); - - // Sets the border of a button. - static void SetBorder(ContentPresenter? presenter, Func apply) - { - if ((presenter?.Content as MultiButtonBarControl.ButtonWrapper)? - .Control?.FindName("ButtonBorder") is Border border) - { - border.CornerRadius = apply(border.CornerRadius); - } - } - - // Reset all the borders. - foreach (ContentPresenter contentPresenter in positions.Keys) - { - SetBorder(contentPresenter, c => default); - } - - // Top-left. - SetBorder(positions - .OrderBy(p => p.Value.X) - .ThenBy(p => p.Value.Y) - .FirstOrDefault().Key, cr => - { - cr.TopLeft = this.CornerRadius; - return cr; - }); - - // Top-right. - SetBorder(positions - .OrderByDescending(p => p.Value.X) - .ThenBy(p => p.Value.Y) - .FirstOrDefault().Key, cr => - { - cr.TopRight = this.CornerRadius; - return cr; - }); - - // Bottom-left. - SetBorder(positions - .OrderBy(p => p.Value.X) - .ThenByDescending(p => p.Value.Y) - .FirstOrDefault().Key, cr => - { - cr.BottomLeft = this.CornerRadius; - return cr; - }); - - // Bottom-right. - SetBorder(positions - .OrderByDescending(p => p.Value.X) - .ThenByDescending(p => p.Value.Y) - .FirstOrDefault().Key, cr => - { - cr.BottomRight = this.CornerRadius; - return cr; - }); - } - } - } -} diff --git a/Morphic.Client/Bar/UI/BarControls/ThemeHandler.cs b/Morphic.Client/Bar/UI/BarControls/ThemeHandler.cs deleted file mode 100644 index 29258fae..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ThemeHandler.cs +++ /dev/null @@ -1,153 +0,0 @@ -namespace Morphic.Client.Bar.UI.BarControls -{ - using System; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Controls.Primitives; - using System.Windows.Input; - using System.Windows.Media; - using Data; - - public class ThemeHandler - { - public Control Control { get; } - public BarItemTheme Theme { get; } - - /// - /// Current theme to use, depending on the state (normal/hover/focus). - /// - public Theme ActiveTheme { get; private set;} - public bool IsMouseDown { get;set;} - public bool FocusedByKeyboard { get;set;} - public bool IsChecked { get; set; } - - public event EventHandler? ThemeStateChanged; - - public ThemeHandler(Control control, BarItemTheme theme) - { - this.Control = control; - this.Theme = theme; - this.ActiveTheme = this.Theme; - if (this.Control.IsLoaded) - { - this.ControlOnLoaded(this.Control, EventArgs.Empty); - } - else - { - this.Control.Loaded += this.ControlOnLoaded; - } - } - - private void ControlOnLoaded(object sender, EventArgs e) - { - this.ApplyTheme(); - this.UpdateTheme(); - } - - /// - /// Apply theming to the control. - /// - /// - protected void ApplyTheme() - { - // Some events to monitor the state. - this.Control.MouseEnter += (sender, args) => this.UpdateTheme(); - this.Control.MouseLeave += (sender, args) => - { - this.CheckMouseState(sender, args); - this.UpdateTheme(); - }; - - this.Control.PreviewMouseDown += this.CheckMouseState; - this.Control.PreviewMouseUp += this.CheckMouseState; - - this.Control.IsKeyboardFocusWithinChanged += (sender, args) => - { - this.FocusedByKeyboard = this.Control.IsKeyboardFocusWithin && - InputManager.Current.MostRecentInputDevice is KeyboardDevice; - this.UpdateTheme(); - }; - - if (this.Control is ToggleButton button) - { - this.IsChecked = button.IsChecked == true; - button.Checked += this.ButtonCheckedChange; - button.Unchecked += this.ButtonCheckedChange; - } - } - - private void ButtonCheckedChange (object sender, RoutedEventArgs e) - { - if (sender is ToggleButton button) - { - this.IsChecked = button.IsChecked ?? false; - this.UpdateTheme(); - } - } - - - /// - /// Update the theme depending on the current state of the control. - /// - public void UpdateTheme() - { - Theme theme = new Theme(); - - // Apply the applicable states, most important first. - if (this.IsMouseDown) - { - theme.Apply(this.Theme.Active); - } - - if (this.Control.IsMouseOver) - { - theme.Apply(this.Theme.Hover); - } - - if (this.Control.IsKeyboardFocusWithin && this.FocusedByKeyboard) - { - theme.Apply(this.Theme.Focus); - } - - if (this.IsChecked) - { - theme.Apply(this.Theme.Checked); - } - - //this.ActiveTheme = theme.Apply(this.Theme); - this.ActiveTheme = theme.Apply(this.Theme); - - this.ThemeStateChanged?.Invoke(this, new ThemeEventArgs(this.ActiveTheme)); - - // Update the brush used by a mono drawing image. - if (this.DrawingBrush is not null && this.ActiveTheme.Background.HasValue) - { - this.DrawingBrush.Color = this.ActiveTheme.Background.Value; - } - } - - public SolidColorBrush? DrawingBrush { get; } - - - private void CheckMouseState(object sender, MouseEventArgs mouseEventArgs) - { - bool last = this.IsMouseDown; - this.IsMouseDown = mouseEventArgs.LeftButton == MouseButtonState.Pressed; - if (last != this.IsMouseDown) - { - this.UpdateTheme(); - } - } - - } - - public class ThemeEventArgs : EventArgs - { - public Theme ActiveTheme { get; } - - public ThemeEventArgs(Theme activeTheme) - { - this.ActiveTheme = activeTheme; - } - } -} diff --git a/Morphic.Client/Bar/UI/BarWindow.xaml b/Morphic.Client/Bar/UI/BarWindow.xaml deleted file mode 100644 index 9e108823..00000000 --- a/Morphic.Client/Bar/UI/BarWindow.xaml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml.cs b/Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml.cs deleted file mode 100644 index ecd9660b..00000000 --- a/Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml.cs +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Client.Dialogs -{ - using System; - using System.Text.RegularExpressions; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Controls; - using Core; - using Elements; - using Microsoft.Extensions.Logging; - using Service; - using MessageBox = System.Windows.Forms.MessageBox; - - /// - /// A panel shown when the user needs to create an account in order to save their captured settings - /// - public partial class CreateAccountPanel : StackPanel, IStepPanel - { - - #region Creating a Panel - - public CreateAccountPanel(MorphicSession morphicSession, ILogger logger, IServiceProvider serviceProvider) - { - this.morphicSession = morphicSession; - this.logger = logger; - this.serviceProvider = serviceProvider; - this.InitializeComponent(); - } - - /// - /// A logger to use - /// - private readonly ILogger logger; - - private readonly IServiceProvider serviceProvider; - - #endregion - - #region Completion Events - - /// - /// Dispatched when the user's accout is successfully created - /// - public event EventHandler? Completed; - - #endregion - - public bool ApplyPreferencesAfterLogin { get; set; } = false; - - #region User Info - - public Preferences? Preferences = null; - - #endregion - - #region Form Submission - - /// - /// The session to use for making requests - /// - private readonly MorphicSession morphicSession; - - /// - /// Event handler for the submit button click - /// - /// - /// - public async void OnSubmit(object? sender, RoutedEventArgs e) - { - await this.SubmitAsync(); - } - - /// - /// An async method for actually doing the registration submission - /// - /// - private async Task SubmitAsync() - { - // TODO: show activity indicator - this.UpdateValidation(); - this.SetFieldsEnabled(false); - var user = new User(); - user.Email = this.UsernameField.Text; - var credentials = new UsernameCredentials(this.UsernameField.Text, this.PasswordField.Password); - var success = false; - var errorMessage = ""; - try - { - success = await this.morphicSession.RegisterUserAsync(user, credentials, this.Preferences ?? new Preferences(), true); - } - catch (AuthService.BadPasswordException) - { - errorMessage = "Your password is too easily guessed. Please use another."; - } - catch (AuthService.ExistingEmailException) - { - errorMessage = "We recognize your email. Use the 'Already have an account?' link below."; - } - catch (AuthService.ExistingUsernameException) - { - errorMessage = "We recognize your email. Use the 'Already have an account?' link below."; - } - catch (AuthService.InvalidEmailException) - { - errorMessage = "Please provide a valid email address"; - } - if (success) - { - MessageBox.Show("Your account was created.\n\nPlease check your email for further instructions."); - this.Completed?.Invoke(this, new EventArgs()); - } - else - { - if (errorMessage == "") - { - errorMessage = "We could not complete the request. Please try again."; - } - this.ErrorLabel.Visibility = Visibility.Visible; - this.ErrorLabel.Content = errorMessage; - this.ErrorLabel.Focus(); // Causes screen reader to read label - this.SetFieldsEnabled(true); - } - } - - #endregion - - #region Input Validation - - /// - /// The various client-checked input errors the user may encounter - /// - private enum ValidationError - { - None, - EmptyUsername, - EmptyPassword, - EmptyConfirmation, - UsernameTooShort, - PasswordTooShort, - PasswordsDontMatch - } - - /// - /// Get the most revelant input error for the user - /// - private ValidationError inputError { - get - { - var username = this.UsernameField.Text; - var password = this.PasswordField.Password; - var confirmation = this.ConfirmPasswordField.Password; - if (!this.hasTypedUsername) - { - return ValidationError.EmptyUsername; - } - if (username.Length < minimumUsernameLength) - { - return ValidationError.UsernameTooShort; - } - if (!this.hasTypedPassword) - { - return ValidationError.EmptyPassword; - } - if (password.Length < minimumPasswordLength) - { - return ValidationError.PasswordTooShort; - } - if (!this.hasTypedConfirmation) - { - return ValidationError.EmptyConfirmation; - } - if (password != confirmation) - { - return ValidationError.PasswordsDontMatch; - } - return ValidationError.None; - } - } - - /// - /// A client-enforced username minimum length - /// - private const int minimumUsernameLength = 2; - - /// - /// A client-enforced password minimum length - /// - private const int minimumPasswordLength = 6; - - /// - /// Indicates if the username field has been typed in, which is used to decide which errors to show - /// - private bool hasTypedUsername; - - /// - /// Indicates if the password field has been typed in, which is used to decide which errors to show - /// - private bool hasTypedPassword; - - /// - /// Indicates if the password confirmation has been typed in, which is used to decide which errors to show - /// - private bool hasTypedConfirmation; - - /// - /// Enable or disable all the fields - /// - /// - private void SetFieldsEnabled(bool enabled) - { - this.UsernameField.IsEnabled = enabled; - this.PasswordField.IsEnabled = enabled; - this.SubmitButton.IsEnabled = enabled; - } - - /// - /// Update the UI based on the current input validation state - /// - private void UpdateValidation() - { - var error = this.inputError; - this.SubmitButton.IsEnabled = error == ValidationError.None; - switch (error) { - case ValidationError.UsernameTooShort: - this.ErrorLabel.Content = String.Format("Your username needs to be at least {0} letters", minimumUsernameLength); - this.ErrorLabel.Visibility = Visibility.Visible; - break; - case ValidationError.PasswordTooShort: - this.ErrorLabel.Content = String.Format("Your password needs to be at least {0} letters", minimumPasswordLength); - this.ErrorLabel.Visibility = Visibility.Visible; - break; - case ValidationError.PasswordsDontMatch: - this.ErrorLabel.Content = "Your passwords don't match"; - this.ErrorLabel.Visibility = Visibility.Visible; - break; - default: - this.ErrorLabel.Visibility = Visibility.Hidden; - break; - } - - } - - /// - /// Used to remove whitespace from the username field - /// - private static Regex whitespaceExpression = new Regex(@"\s"); - - private void UsernameField_TextChanged(object sender, TextChangedEventArgs e) - { - this.UsernameField.Text = whitespaceExpression.Replace(this.UsernameField.Text, ""); - this.UsernameField.SelectionStart = this.UsernameField.Text.Length; - this.UsernameField.SelectionLength = 0; - this.UpdateValidation(); - } - - private void PasswordField_PasswordChanged(object sender, RoutedEventArgs e) - { - this.UpdateValidation(); - } - - private void UsernameField_LostFocus(object sender, RoutedEventArgs e) - { - this.hasTypedUsername = this.UsernameField.Text.Length > 0; - this.UpdateValidation(); - } - - private void PasswordField_LostFocus(object sender, RoutedEventArgs e) - { - this.hasTypedPassword = this.PasswordField.Password.Length > 0; - this.UpdateValidation(); - } - - private void ConfirmPasswordField_PasswordChanged(object sender, RoutedEventArgs e) - { - this.hasTypedConfirmation = this.ConfirmPasswordField.Password == this.PasswordField.Password; - this.UpdateValidation(); - } - - private void ConfirmPasswordField_LostFocus(object sender, RoutedEventArgs e) - { - this.hasTypedConfirmation = this.ConfirmPasswordField.Password.Length > 0; - this.UpdateValidation(); - } - - #endregion - - #region Other Actions - - /// - /// Handler for when the user clicks on the "Already have an Account?" button - /// - /// - /// - public void OnAlreadyHaveAccount(object? sender, RoutedEventArgs e) - { - LoginPanel loginPanel = this.StepFrame.PushPanel(); - loginPanel.ApplyPreferencesAfterLogin = this.ApplyPreferencesAfterLogin; - } - - #endregion - - public StepFrame StepFrame { get; set; } - - private void StackPanel_Loaded(object sender, RoutedEventArgs e) - { - // set focus to the username field once this panel is loaded - this.UsernameField.Focus(); - } - } -} diff --git a/Morphic.Client/Dialogs/Travel/RestoreWindow.xaml b/Morphic.Client/Dialogs/Travel/RestoreWindow.xaml deleted file mode 100644 index 6fab058c..00000000 --- a/Morphic.Client/Dialogs/Travel/RestoreWindow.xaml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - Restore settings from backup - - The last time Morphic applied a user’s settings to this user account on this computer, Morphic captured the - settings already on the computer and saved them to a backup. - - - Backup Date: - - - Clicking "Restore from backup" below will restore those settings to this computer. - - - - - - diff --git a/Morphic.Client/Dialogs/Travel/TravelCompletedPanel.xaml.cs b/Morphic.Client/Dialogs/Travel/TravelCompletedPanel.xaml.cs deleted file mode 100644 index 184cecd0..00000000 --- a/Morphic.Client/Dialogs/Travel/TravelCompletedPanel.xaml.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Client.Dialogs -{ - using System; - using System.Windows; - using System.Windows.Controls; - using Microsoft.Extensions.Logging; - using Service; - - /// - /// Shown at the end of the capture process as a review for the user - /// - public partial class TravelCompletedPanel : StackPanel - { - - #region Creating a Panel - - public TravelCompletedPanel(MorphicSession morphicSession, ILogger logger) - { - this.morphicSession = morphicSession; - this.logger = logger; - this.InitializeComponent(); - } - - /// - /// A logger to use - /// - private readonly ILogger logger; - - #endregion - - #region Completion Events - - /// - /// The event that is dispatched when the user clicks the Close button - /// - public event EventHandler? Completed; - - #endregion - - #region Lifecycle - - private readonly MorphicSession morphicSession; - - protected override void OnInitialized(EventArgs e) - { - base.OnInitialized(e); - this.Loaded += this.OnLoaded; - } - - private void OnLoaded(object sender, RoutedEventArgs e) - { - this.EmailLabel.Content = this.morphicSession.User?.Email; - } - - #endregion - - #region Actions - - /// - /// Handler for when the user clicks the Close button - /// - /// - /// - private void OnClose(object? sender, RoutedEventArgs e) - { - this.Completed?.Invoke(this, new EventArgs()); - } - - #endregion - } -} diff --git a/Morphic.Client/Dialogs/Travel/TravelWindow.xaml b/Morphic.Client/Dialogs/Travel/TravelWindow.xaml deleted file mode 100644 index 4da137ce..00000000 --- a/Morphic.Client/Dialogs/Travel/TravelWindow.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - diff --git a/Morphic.Client/Dialogs/Travel/TravelWindow.xaml.cs b/Morphic.Client/Dialogs/Travel/TravelWindow.xaml.cs deleted file mode 100644 index e1e42ef8..00000000 --- a/Morphic.Client/Dialogs/Travel/TravelWindow.xaml.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Client.Dialogs -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Windows; - using Microsoft.Extensions.Logging; - using Service; - - /// - /// Window that walks the user the the capture and, if necessary, account creation process. - /// Loads each panel one at time depending on what steps are required - /// - public partial class TravelWindow : Window, MorphicWindowWithArgs - { - - #region Create a Window - - public TravelWindow(MorphicSession morphicSession, ILogger logger, IServiceProvider serviceProvider) - { - this.morphicSession = morphicSession; - this.logger = logger; - this.serviceProvider = serviceProvider; - this.InitializeComponent(); - } - - /// - /// The Morphic session to consult when making decisions - /// - private readonly MorphicSession morphicSession; - - /// - /// A logger to use - /// - private readonly ILogger logger; - - /// - /// A service provider to use when creating panels - /// - private readonly IServiceProvider serviceProvider; - - #endregion - - private string? StartPanelAction; - - protected override void OnInitialized(EventArgs e) - { - base.OnInitialized(e); - this.ShowStartPanel(); - } - - private void ShowStartPanel() - { - CopyStartPanel copyStartPanel = this.StepFrame.PushPanel(); - copyStartPanel.Completed += (sender, args) => this.Close(); - } - - void MorphicWindowWithArgs.SetArguments(Dictionary args) - { - foreach (var (key, value) in args) - { - switch (key.ToLower()) - { - case "action": - this.StartPanelAction = (value as string) ?? null; - break; - default: - Debug.Assert(false, "Unknown argument"); - break; - } - } - } - - private void Window_Loaded(object sender, RoutedEventArgs e) - { - if (this.StartPanelAction is not null) - { - var copyStartPanel = this.StepFrame.CurrentPanel as CopyStartPanel; - - switch (this.StartPanelAction) - { - case "CopyToCloud": - copyStartPanel?.CopyToCloud(sender /* or null */, new RoutedEventArgs()); - break; - case "CopyFromCloud": - copyStartPanel?.CopyFromCloud(sender /* or null */, new RoutedEventArgs()); - break; - default: - Debug.Assert(false, "Unknown action"); - break; - } - } - } - } -} diff --git a/Morphic.Client/Icon.ico b/Morphic.Client/Icon.ico deleted file mode 100644 index 633cab33..00000000 Binary files a/Morphic.Client/Icon.ico and /dev/null differ diff --git a/Morphic.Client/Icon.png b/Morphic.Client/Icon.png deleted file mode 100644 index 497994e4..00000000 Binary files a/Morphic.Client/Icon.png and /dev/null differ diff --git a/Morphic.Client/Menu/MorphicHybridTrayIcon.cs b/Morphic.Client/Menu/MorphicHybridTrayIcon.cs deleted file mode 100644 index 0de595c3..00000000 --- a/Morphic.Client/Menu/MorphicHybridTrayIcon.cs +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Client.Menu -{ - using System; - using System.ComponentModel; - using System.Diagnostics; - using System.Drawing; - using System.IO; - using System.Reflection; - using System.Runtime.InteropServices; - using System.Windows.Forms; - using Bar.UI; - - /// - /// Displays a system tray icon (NotifyIcon) in the notification area and/or an always-visible - /// button (MorphicTrayButton) next to the notification area on the task bar. - /// - public class MorphicHybridTrayIcon : IDisposable - { - private Icon? _icon = null; - private string? _text = null; - private bool _visible = false; - - // Used if a tray icon is desired instead of a next-to-tray taskbar button - private NotifyIcon? _notifyIcon = null; - - // Used if a next-to-tray button is desired instead of a tray icon - private MorphicTrayButton? _trayButton = null; - - public enum TrayIconLocationOption - { - None, - NotificationTray, - NextToNotificationTray, - NotificationTrayAndNextToNotificationTray - } - - private TrayIconLocationOption _trayIconLocation = TrayIconLocationOption.None; - - /// Raised when the button is clicked. - public event EventHandler? Click; - /// Raised when the button is right-clicked. - public event EventHandler? SecondaryClick; - - public MorphicHybridTrayIcon() - { - } - - public void Dispose() - { - _notifyIcon?.Dispose(); - _notifyIcon = null; - - _trayButton?.Dispose(); - _trayButton = null; - } - - /// The icon for the tray icon - public Icon? Icon - { - get - { - return _icon; - } - set - { - _icon = value; - if (_notifyIcon is not null) - { - _notifyIcon.Icon = _icon; - } - if (_trayButton is not null) - { - _trayButton.Icon = _icon; - } - } - } - - /// Tooltip for the tray icon. - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - if (_notifyIcon is not null) - { - _notifyIcon.Text = _text; - } - if (_trayButton is not null) - { - _trayButton.Text = _text; - } - } - } - - /// Show or hide the tray icon. - public bool Visible - { - get - { - return _visible; - } - set - { - _visible = value; - - if (_notifyIcon is not null) - { - _notifyIcon.Visible = _visible; - } - if (_trayButton is not null) - { - _trayButton.Visible = _visible; - } - } - } - - // - - private void InitializeTrayIcon() - { - if (_notifyIcon is not null) { - return; - } - - _notifyIcon = new NotifyIcon(); - _notifyIcon.Text = _text; - _notifyIcon.Icon = _icon; - // - _notifyIcon.MouseUp += (sender, args) => - { - if (args.Button == MouseButtons.Right) - { - this.SecondaryClick?.Invoke(this, args); - } - else if (args.Button == MouseButtons.Left) - { - this.Click?.Invoke(this, args); - } - }; - _notifyIcon.Visible = _visible; - } - - private void InitializeTrayButton() - { - if (_trayButton is not null) - { - return; - } - - _trayButton = new MorphicTrayButton(); - _trayButton.Text = _text; - _trayButton.Icon = _icon; - // - _trayButton.MouseUp += (sender, args) => - { - if (args.Button == MouseButtons.Right) - { - this.SecondaryClick?.Invoke(this, args); - } - else if (args.Button == MouseButtons.Left) - { - this.Click?.Invoke(this, args); - } - }; - _trayButton.Visible = _visible; - } - - // - - public TrayIconLocationOption TrayIconLocation - { - get - { - return _trayIconLocation; - } - set - { - _trayIconLocation = value; - - // create notify icon if requested - switch (value) - { - case TrayIconLocationOption.NotificationTray: - case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: - if (_notifyIcon is null) - { - this.InitializeTrayIcon(); - } - break; - } - - // create tray button if requested - switch (value) - { - case TrayIconLocationOption.NextToNotificationTray: - case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: - if (_trayButton is null) - { - this.InitializeTrayButton(); - } - break; - } - - // destroy notify icon if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NextToNotificationTray: - if (_notifyIcon is not null) - { - _notifyIcon.Dispose(); - _notifyIcon = null; - } - break; - } - - // destroy tray button if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NotificationTray: - if (_trayButton is not null) - { - _trayButton.Dispose(); - _trayButton = null; - } - break; - } - } - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Menu/MorphicMenu.xaml b/Morphic.Client/Menu/MorphicMenu.xaml deleted file mode 100644 index ba3638c7..00000000 --- a/Morphic.Client/Menu/MorphicMenu.xaml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/Menu/MorphicMenu.xaml.cs b/Morphic.Client/Menu/MorphicMenu.xaml.cs deleted file mode 100644 index 6aafabbf..00000000 --- a/Morphic.Client/Menu/MorphicMenu.xaml.cs +++ /dev/null @@ -1,433 +0,0 @@ -namespace Morphic.Client.Menu -{ - using Bar.UI; - using CountlySDK; - using Morphic.Client.Config; - using Morphic.Client.Dialogs; - using Morphic.Windows.Native.OsVersion; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Controls; - using System.Windows.Controls.Primitives; - using Windows.Native.Input; - - public partial class MorphicMenu : ContextMenu - { - internal enum MenuOpenedSource - { - trayIcon, - morphicBarIcon - } - private MenuOpenedSource? _menuOpenedSource; - - public App App => App.Current; - - public MorphicMenu() - { - this.DataContext = this; - this.InitializeComponent(); - } - - protected override void OnInitialized(EventArgs e) - { - // if ConfigurableFeatures.CloudSettingsTransferIsEnabled is false, then hide the settings which can transfer/restore settings - if (ConfigurableFeatures.CloudSettingsTransferIsEnabled == false) - { - this.ChangeSetupMenuItem.Visibility = Visibility.Collapsed; - this.SaveMySetupMenuItem.Visibility = Visibility.Collapsed; - this.RestoreSettingsFromBackupMenuItem.Visibility = Visibility.Collapsed; - this.CloudSettingsSeparator.Visibility = Visibility.Collapsed; - } - - this.ShowTrayIcon(); - base.OnInitialized(e); - } - - protected override void OnOpened(RoutedEventArgs e) - { - // if autorun settings are configured by config.json, do not give the user the option to enable/disable - if (ConfigurableFeatures.AutorunConfig is not null) - { - this.AutorunAfterLoginItem.Visibility = Visibility.Collapsed; - } - - // if morphicBarVisibilityAfterLogin settings are configured by config.json, do not give the user the option to enable/disable - if (ConfigurableFeatures.MorphicBarVisibilityAfterLogin is not null) - { - this.ShowMorphicBarAfterLoginItem.Visibility = Visibility.Collapsed; - } - - this.ShowBar.Visibility = (!this.App.BarManager.BarVisible).ToVisibility(); - this.HideBar.Visibility = this.App.BarManager.BarVisible.ToVisibility(); - - this.LoginItem.Visibility = (!this.App.MorphicSession.SignedIn).ToVisibility(); - this.LogoutItem.Visibility = this.App.MorphicSession.SignedIn.ToVisibility(); - - base.OnOpened(e); - } - - internal async Task ShowAsync(Control? control = null, MenuOpenedSource? menuOpenedSource = null) - { - _menuOpenedSource = menuOpenedSource; - - if (control is null) - { - this.Placement = PlacementMode.Mouse; - this.PlacementTarget = null; - } - else - { - this.Placement = PlacementMode.Top; - this.PlacementTarget = control; - } - - this.IsOpen = true; - - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("showMenu", 1, segmentation); - } - - private CountlySDK.Segmentation CreateMenuOpenedSourceSegmentation(MenuOpenedSource? menuOpenedSource) - { - var segmentation = new CountlySDK.Segmentation(); - if (_menuOpenedSource is not null) - { - segmentation.Add("eventSource", _menuOpenedSource.ToString() + "Menu"); - } - return segmentation; - } - - private async void ShowBarClick(object sender, RoutedEventArgs e) - { - this.App.BarManager.ShowBar(); - // - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("morphicBarShow", 1, segmentation); - } - - private async void HideBarClick(object sender, RoutedEventArgs e) - { - this.App.BarManager.HideBar(); - // - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("morphicBarHide", 1, segmentation); - } - - private async void QuitClick(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("quit", 1, segmentation); - - this.App.BarManager.CloseBar(); - this.App.Shutdown(); - } - - private async void AutorunAfterLoginClicked(object sender, RoutedEventArgs e) - { - switch (AutorunAfterLoginItem.IsChecked) - { - case true: - await App.Current.Countly_RecordEventAsync("autorunAfterLoginEnabled"); - break; - case false: - await App.Current.Countly_RecordEventAsync("autorunAfterLoginDisabled"); - break; - } - } - - private async void ShowMorphicBarAfterLoginClicked(object sender, RoutedEventArgs e) - { - switch (ShowMorphicBarAfterLoginItem.IsChecked) - { - case true: - await App.Current.Countly_RecordEventAsync("showMorphicBarAfterLoginEnabled"); - break; - case false: - await App.Current.Countly_RecordEventAsync("showMorphicBarAfterLoginDisabled"); - break; - } - } - - private async void WindowsSettingsAllAccessibilityOptionsClicked(object sender, RoutedEventArgs e) - { - string settingsUrlAsPath = null!; // required to quiet the "not initialized error" - var windowsVersion = OsVersion.GetWindowsVersion(); - - if (windowsVersion is not null) - { - switch (windowsVersion) - { - case WindowsVersion.Win10_v1809: - case WindowsVersion.Win10_v1903: - case WindowsVersion.Win10_v1909: - case WindowsVersion.Win10_v2004: - case WindowsVersion.Win10_v20H2: - case WindowsVersion.Win10_v21H1: - case WindowsVersion.Win10_v21H2: - // Windows 10 1809, 1903, 1909, 2004, 20H2, 21H1, 21H2 - // NOTE: we should re-evaluate this path in all versions of Windows (to verify that it shouldn't be simply "ms-settings:easeofaccess" instead) - settingsUrlAsPath = "ms-settings:easeofaccess-display"; - break; - case WindowsVersion.Win10_vFuture: - // OBSERVATION: this may be the wrong path for future verisons of Windows (especially since Win10 and Win11 _may_ treat this differently post-21H1); re-evaluate this logic - settingsUrlAsPath = "ms-settings:easeofaccess-display"; - break; - case WindowsVersion.Win11_v21H2: - case WindowsVersion.Win11_vFuture: - // Windows 11 21H2 (and assumed for the future) - settingsUrlAsPath = "ms-settings:easeofaccess"; - break; - default: - // not supported - Debug.Assert(false, "This build of Windows is not supported"); - return; - } - } - else - { - // not supported - Debug.Assert(false, "This build of Windows is not supported"); - return; - } - - MorphicMenuItem.OpenMenuItemPath(settingsUrlAsPath); - await MorphicMenuItem.RecordMenuItemTelemetryAsync(settingsUrlAsPath, ((MorphicMenuItem)sender).ParentMenuType, ((MorphicMenuItem)sender).TelemetryType, ((MorphicMenuItem)sender).TelemetryCategory); - } - - private async void WindowsSettingsPointerSizeClicked(object sender, RoutedEventArgs e) - { - string settingsUrlAsPath = null!; // required to quiet the "not initialized error" - var windowsVersion = OsVersion.GetWindowsVersion(); - - if (windowsVersion is not null) - { - switch (windowsVersion) - { - case WindowsVersion.Win10_v1809: - case WindowsVersion.Win10_v1903: - case WindowsVersion.Win10_v1909: - // Windows 10 1809, 1903, 1909 - settingsUrlAsPath = "ms-settings:easeofaccess-cursorandpointersize"; - break; - case WindowsVersion.Win10_v2004: - // Windows 10 2004 - settingsUrlAsPath = "ms-settings:easeofaccess-MousePointer"; - break; - case WindowsVersion.Win10_v20H2: - // Windows 10 20H2 - // NOTE: Microsoft changed the URL for this link somwhere between 10.0.19042.986 and 10.0.19042.1052; - // if we get any bug reports that this link doesn't work with v20H2, be sure to get the "winver" full version #...so we can adjust the revision # below (to something between 986 and 1051) as appropriate - uint? updateBuildRevision; - var getUpdateBuildRevisionResult = Morphic.Windows.Native.OsVersion.OsVersion.GetUpdateBuildRevision(); - if (getUpdateBuildRevisionResult.IsSuccess == true) - { - updateBuildRevision = getUpdateBuildRevisionResult.Value!; - } - else - { - // NOTE: if we could not get the update build revision, we fail gracefully by assuming that the user's computer is updated to the OS version's most recent updates - updateBuildRevision = null; - } - - if (updateBuildRevision.HasValue == true && updateBuildRevision.Value < 1052) - { - // NOTE: this link was verified in Windows 10 19042.985 - settingsUrlAsPath = "ms-settings:easeofaccess-MousePointer"; - } - else - { - // NOTE: this link was verified in Windows 10 19042.1052 - settingsUrlAsPath = "ms-settings:easeofaccess-mousepointer"; - } - break; - case WindowsVersion.Win10_v21H1: - case WindowsVersion.Win10_v21H2: - case WindowsVersion.Win10_vFuture: - // Windows 10 21H1, Windows 10 21H2 (and assumed for the future) - settingsUrlAsPath = "ms-settings:easeofaccess-mousepointer"; - break; - case WindowsVersion.Win11_v21H2: - case WindowsVersion.Win11_vFuture: - // Windows 11 21H2 (and assumed for the future) - settingsUrlAsPath = "ms-settings:easeofaccess-mousepointer"; - break; - default: - // not supported - Debug.Assert(false, "This build of Windows is not supported"); - return; - } - } - else - { - // not supported - Debug.Assert(false, "This build of Windows is not supported"); - return; - } - - MorphicMenuItem.OpenMenuItemPath(settingsUrlAsPath); - await MorphicMenuItem.RecordMenuItemTelemetryAsync(settingsUrlAsPath, ((MorphicMenuItem)sender).ParentMenuType, ((MorphicMenuItem)sender).TelemetryType, ((MorphicMenuItem)sender).TelemetryCategory); - } - - private void StopKeyRepeatInit(object sender, RoutedEventArgs e) - { - if (sender is MenuItem menuItem) - { - menuItem.IsChecked = Keyboard.KeyRepeat(); - } - } - // - private async void StopKeyRepeatToggle(object sender, RoutedEventArgs e) - { - if (sender is MenuItem menuItem) - { - menuItem.IsChecked = Keyboard.KeyRepeat(menuItem.IsChecked); - - if (menuItem.IsChecked == true) - { - await App.Current.Countly_RecordEventAsync("stopKeyRepeatOn"); - } - else - { - await App.Current.Countly_RecordEventAsync("stopKeyRepeatOff"); - } - } - } - - #region TrayIcon - - private MorphicHybridTrayIcon? _trayIcon = null; - - private void ShowTrayIcon() - { - // TODO: re-implement using solutions registry. - // SystemSetting filterType = new SystemSetting("SystemSettings_Notifications_ShowIconsOnTaskbar", - // new LoggerFactory().CreateLogger()); - // var allNotificationIconsShown = (await filterType.GetValue() as bool? == true) ? TrayIcon.TrayIconLocationOption.NotificationTray : TrayIcon.TrayIconLocationOption.NextToNotificationTry; - - WindowMessageHook windowMessageHook = WindowMessageHook.GetGlobalMessageHook(); - MorphicHybridTrayIcon trayIcon = new MorphicHybridTrayIcon(); - trayIcon = new MorphicHybridTrayIcon(); - trayIcon.Click += this.OnTrayIconClicked; - trayIcon.SecondaryClick += this.OnTrayIconRightClicked; - trayIcon.Icon = Client.Properties.Resources.Icon; - trayIcon.Text = "Morphic"; - //trayIcon.TrayIconLocation = allNotificationIconsShown; - trayIcon.TrayIconLocation = MorphicHybridTrayIcon.TrayIconLocationOption.NextToNotificationTray; - trayIcon.Visible = true; - _trayIcon = trayIcon; - - this.App.Exit += (sender, args) => - { - _trayIcon.Visible = false; - _trayIcon.Dispose(); - _trayIcon = null; - }; - } - - private async void OnTrayIconRightClicked(object? sender, EventArgs e) - { - await this.Dispatcher.InvokeAsync(async () => - { - await this.ShowAsync(null, MenuOpenedSource.trayIcon); - }); - } - - private async void OnTrayIconClicked(object? sender, EventArgs e) - { - await this.Dispatcher.InvokeAsync(async () => - { - if (this.App.BarManager.BarVisible) - { - this.App.BarManager.HideBar(); - // - var segmentation = new CountlySDK.Segmentation(); - segmentation.Add("eventSource", "trayIconClick"); - await App.Current.Countly_RecordEventAsync("morphicBarHide", 1, segmentation); - } - else - { - this.App.BarManager.ShowBar(); - // - var segmentation = new CountlySDK.Segmentation(); - segmentation.Add("eventSource", "trayIconClick"); - await App.Current.Countly_RecordEventAsync("morphicBarShow", 1, segmentation); - } - }); - } - - #endregion - - private async void Logout(object sender, RoutedEventArgs e) - { - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - await App.Current.MorphicSession.SignOut(); - } - - private async void Login(object sender, RoutedEventArgs e) - { - // NOTE: if we want the login menu item to apply cloud-saved preferences after login, we should set this flag to true - var applyPreferencesAfterLogin = ConfigurableFeatures.CloudSettingsTransferIsEnabled; - var args = new Dictionary() { { "applyPreferencesAfterLogin", applyPreferencesAfterLogin } }; - await App.Current.Dialogs.OpenDialogAsync(args); - } - - private async void CustomizeMorphicbarClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("customizeMorphicbar", 1, segmentation); - - // NOTE: when we make "navigate to URL" a custom action (rather than something linked in the menu itself), then we should navigate to the appsettings value for the key"BarEditorWebAppUrlAsString" - } - - private async void ContactUsClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("contactUs", 1, segmentation); - } - - private async void ExploreMorphicClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("exploreMorphic", 1, segmentation); - } - - private async void HowToCopySetupsClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("howToCopySetups", 1, segmentation); - } - - private async void QuickDemoVideosClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - segmentation.Add("category", "main"); - await App.Current.Countly_RecordEventAsync("quickDemoVideo", 1, segmentation); - } - - private async void OtherHelpfulThingsClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("otherHelpfulThings", 1, segmentation); - } - - private async void AboutMorphicClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await App.Current.Countly_RecordEventAsync("aboutMorphic", 1, segmentation); - } - - private void SelectBasicMorphicBarClick(object sender, RoutedEventArgs e) - { - AppOptions.Current.LastCommunity = null; - AppOptions.Current.LastMorphicbarId = null; - App.Current.BarManager.LoadBasicMorphicBar(); - } - } - - -} - diff --git a/Morphic.Client/Menu/MorphicMenuItem.cs b/Morphic.Client/Menu/MorphicMenuItem.cs deleted file mode 100644 index 27dd0e5b..00000000 --- a/Morphic.Client/Menu/MorphicMenuItem.cs +++ /dev/null @@ -1,225 +0,0 @@ -namespace Morphic.Client.Menu -{ - using Config; - using CountlySDK; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Threading.Tasks; - using System.Windows; - using System.Windows.Controls; - - public class MorphicMenuItem : MenuItem - { - /// Shell action to open when the item is clicked. - public string Open { get; set; } - - public enum MenuType - { - contextMenu, - mainMenu - } - public MenuType ParentMenuType = MenuType.mainMenu; - - public Type? Dialog { get; set; } - public string? DialogAction { get; set; } - - public enum MorphicMenuItemTelemetryType - { - Settings, - LearnMore, - QuickDemoVideo - } - public MorphicMenuItemTelemetryType? TelemetryType; - public string? TelemetryCategory; - - public MorphicMenuItem() - { - this.Click += this.OnClick; - } - - internal static void OpenMenuItemPath(string openPath) - { - Process.Start(new ProcessStartInfo(openPath) - { - UseShellExecute = true - }); - } - - internal static async Task RecordMenuItemTelemetryAsync(string? openPath, MorphicMenuItem.MenuType parentMenuType, MorphicMenuItemTelemetryType? telemetryType, string? telemetryCategory) - { - string? eventSource = null; - switch (parentMenuType) - { - case MenuType.mainMenu: - eventSource = "iconMenu"; - break; - case MenuType.contextMenu: - eventSource = "contextMenu"; - break; - } - - - switch (telemetryType) - { - case MorphicMenuItemTelemetryType.Settings: - { - var segmentation = new Segmentation(); - var settingCategoryName = telemetryCategory; - if (settingCategoryName is not null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await App.Current.Countly_RecordEventAsync("systemSettings", 1, segmentation); - //await App.Current.Countly_RecordEventAsync("systemSettings" + settingCategoryName); - } - break; - case MorphicMenuItemTelemetryType.LearnMore: - { - var segmentation = new Segmentation(); - var settingCategoryName = telemetryCategory; - if (settingCategoryName is not null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await App.Current.Countly_RecordEventAsync("learnMore", 1, segmentation); - } - break; - case MorphicMenuItemTelemetryType.QuickDemoVideo: - { - var segmentation = new Segmentation(); - var settingCategoryName = telemetryCategory; - if (settingCategoryName is not null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await App.Current.Countly_RecordEventAsync("quickDemoVideo", 1, segmentation); - } - break; - default: - // handle menu "open settings" items - // NOTE: we may want to create a separate "telemetry type" and embed it in the menu xaml itself (so that we don't have to compare against open paths here) - { - string? settingCategoryName = null; - switch (openPath) - { - case "ms-settings:colors": - settingCategoryName = "darkMode"; - break; - case "ms-settings:display": - settingCategoryName = "textSize"; - break; - case "ms-settings:easeofaccess": - case "ms-settings:easeofaccess-display": - settingCategoryName = "allAccessibility"; - break; - case "ms-settings:easeofaccess-colorfilter": - settingCategoryName = "colorFilter"; - break; - case "ms-settings:easeofaccess-cursorandpointersize": - case "ms-settings:easeofaccess-MousePointer": - settingCategoryName = "pointerSize"; - break; - case "ms-settings:easeofaccess-highcontrast": - settingCategoryName = "highContrast"; - break; - case "ms-settings:easeofaccess-keyboard": - settingCategoryName = "keyboard"; - break; - case "ms-settings:easeofaccess-magnifier": - settingCategoryName = "magnifier"; - break; - case "ms-settings:mousetouchpad": - settingCategoryName = "mouse"; - break; - case "ms-settings:nightlight": - settingCategoryName = "nightMode"; - break; - case "ms-settings:regionlanguage": - settingCategoryName = "language"; - break; - case "ms-settings:speech": - settingCategoryName = "readAloud"; - break; - case null: - // unknown (i.e. no data) - break; - default: - Debug.Assert(false, "Unknown menu item (i.e. no telemetry)"); - break; - } - if (settingCategoryName is not null) - { - var segmentation = new Segmentation(); - segmentation.Add("category", settingCategoryName); - segmentation.Add("eventSource", eventSource); - // - await App.Current.Countly_RecordEventAsync("systemSettings", 1, segmentation); - //await App.Current.Countly_RecordEventAsync("systemSettings" + settingCategoryName); - } - } - break; - } - } - - private async void OnClick(object sender, RoutedEventArgs e) - { - string? openPath = null; - - if (sender is MorphicMenuItem item) - { - if (!string.IsNullOrEmpty(item.Open)) - { - openPath = item.Open; - MorphicMenuItem.OpenMenuItemPath(openPath!); - } - - if (this.Dialog is not null) - { - var args = new Dictionary(); - if (this.DialogAction is not null) - { - args["action"] = this.DialogAction!; - } - await App.Current.Dialogs.OpenDialogAsync(this.Dialog!, args); - } - - } - - await RecordMenuItemTelemetryAsync(openPath, ((MorphicMenuItem)sender).ParentMenuType, ((MorphicMenuItem)sender).TelemetryType, ((MorphicMenuItem)sender).TelemetryCategory); - } - } - - /// - /// A menu header item - a separator with text. - /// - public class MorphicMenuHeader : Separator - { - public string? Header { get; set; } = null; - - public override void EndInit() - { - base.EndInit(); - - // Add the header label - ControlTemplate template = new ControlTemplate(this.GetType()); - FrameworkElementFactory factory = new FrameworkElementFactory(typeof(Label)); - factory.SetValue(ContentControl.ContentProperty, this.Header); - factory.SetValue(Label.FontWeightProperty, FontWeights.Bold); - factory.SetValue(Label.ForegroundProperty, SystemColors.MenuTextBrush); - factory.SetValue(Label.BackgroundProperty, SystemColors.MenuBarBrush); - template.VisualTree = factory; - this.Template = template; - } - } - -} diff --git a/Morphic.Client/Menu/MorphicTrayButton.cs b/Morphic.Client/Menu/MorphicTrayButton.cs deleted file mode 100644 index 86223ec4..00000000 --- a/Morphic.Client/Menu/MorphicTrayButton.cs +++ /dev/null @@ -1,1472 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using Morphic.Windows.Native; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows.Forms; -using System.Windows.Threading; - -// TODO: resize the task button container back to where it started after we hide our tray button -// TODO: sometimes, Windows resizes the taskbar under us (in which case the task bar container runs underneath our button); we need to detect this and re-reposition gracefully -// TODO: add support for high contrast icons -// TODO: in some testing, we temporarily experienced a "spinning wheel" over our icon if the mouse cursor hovers over it (right after startup) - -namespace Morphic.Client.Menu -{ - internal class MorphicTrayButton : IDisposable - { - private Icon? _icon = null; - private string? _text = null; - private bool _visible = false; - - private MorphicTrayButtonNativeWindow? _nativeWindow = null; - - //private bool _highContrastModeIsOn_Cached = false; - - public event MouseEventHandler? MouseUp; - - internal MorphicTrayButton() - { - } - - public void Dispose() - { - this.DestroyNativeWindow(); - } - - /// The icon for the tray button - public Icon? Icon - { - get - { - return _icon; - } - set - { - _icon = value; - - _nativeWindow?.SetIcon(_icon); - } - } - - /// Tooltip for the tray button. - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - - _nativeWindow?.SetText(_text); - } - } - - /// Show or hide the tray button. - public bool Visible - { - get - { - return _visible; - } - set - { - _visible = value; - - if (_visible == true) - { - if (_nativeWindow is null) - { - CreateNativeWindow(); - } - } - else if (_visible == false) - { - if (_nativeWindow is not null) - { - DestroyNativeWindow(); - } - } - } - } - - // NOTE: this throws an exception if it fails to create the native window - private void CreateNativeWindow() - { - // if the tray button window already exists; it cannot be created again - if (_nativeWindow is not null) - { - throw new InvalidOperationException(); - } - - // find the window handle of the Windows taskbar - var taskbarHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) - { - // could not find taskbar - throw new Exception("Could not find taskbar"); - } - - /* TODO: consider cached the current DPI of the taskbar (to track, in case the taskbar DPI changes in the future); we currently calculate the icon size based on - * the height/width of the window, so this check may not be necessary */ - - //// cache the current high contrast on/off state (to track) - //_highContrastModeIsOn_Cached = IsHighContrastModeOn(); - - // create the native window - var nativeWindow = new MorphicTrayButtonNativeWindow(this); - - // initialize the native window; note that we have separated "initialize" into a separate function so that our constructor doesn't throw exceptions on failure - try - { - nativeWindow.Initialize(taskbarHandle); - } - catch (Win32Exception ex) - { - // TODO: consider what exceptions we could get here, how to handle them and how to bubble them up to our caller, etc. - throw; - } - catch (InvalidOperationException) - { - throw; - } - - // set the icon for the native window - nativeWindow.SetIcon(_icon); - // set the (tooltip) text for the native window - nativeWindow.SetText(_text); - - // store the reference to our new native window - _nativeWindow = nativeWindow; - } - - private void DestroyNativeWindow() - { - _nativeWindow?.Dispose(); - _nativeWindow = null; - } - - //private bool IsHighContrastModeOn() - //{ - // var highContrastIsOn = (Spi.Instance.GetHighContrast() & Spi.HighContrastOptions.HCF_HIGHCONTRASTON) != 0; - // return highContrastIsOn; - //} - - #region Tray Button (Native Window) - - private class MorphicTrayButtonNativeWindow: NativeWindow, IDisposable - { - private MorphicTrayButton _owner; - - private IntPtr _tooltipWindowHandle = IntPtr.Zero; - private IntPtr _iconHandle = IntPtr.Zero; - - private string? _tooltipText = null; - private bool _tooltipInfoAdded = false; - - private System.Threading.Timer? _trayButtonPositionCheckupTimer; - private int _trayButtonPositionCheckupTimerCounter = 0; - - [Flags] - private enum TrayButtonVisualStateFlags - { - None = 0, - Hover = 1, - LeftButtonPressed = 2, - RightButtonPressed = 4 - } - private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; - - private Windows.Native.WindowMessageHooks.MouseWindowMessageHook? _mouseHook = null; - - internal MorphicTrayButtonNativeWindow(MorphicTrayButton owner) - { - _owner = owner; - } - - public void Initialize(IntPtr taskbarHandle) - { - const string nativeWindowClassName = "Morphic-TrayButton"; - - // register our custom native window class - var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new WinApi.WndProc(this.WndProcCallback)); - var lpWndClass = new WinApi.WNDCLASSEX - { - cbSize = (uint)Marshal.SizeOf(typeof(WinApi.WNDCLASSEX)), - lpfnWndProc = pointerToWndProcCallback, - lpszClassName = nativeWindowClassName, - hCursor = WinApi.LoadCursor(IntPtr.Zero, (int)WinApi.Cursors.IDC_ARROW) - }; - - var registerClassResult = WinApi.RegisterClassEx(ref lpWndClass); - if (registerClassResult == 0) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - - var windowParams = new CreateParams(); - windowParams.ExStyle = (int)WinApi.WindowStylesEx.WS_EX_TOOLWINDOW; - /* NOTE: as we want to be able to ensure that we're referencing the exact class we just registered, we pass the RegisterClassEx results into the - * CreateWindow function (and we encode that result as a ushort here in a proprietary way) */ - windowParams.ClassName = registerClassResult.ToString(); // nativeWindowClassName; - //windowParams.Caption = nativeWindowClassName; - windowParams.Style = (int)(WinApi.WindowStyles.WS_VISIBLE | WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_CLIPSIBLINGS | WinApi.WindowStyles.WS_TABSTOP); - windowParams.X = 0; - windowParams.Y = 0; - windowParams.Width = 32; - windowParams.Height = 40; - windowParams.Parent = taskbarHandle; - // - // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException, or Win32Exception - this.CreateHandle(windowParams); - - // create the tooltip window (although we won't provide it with any actual text until/unless the text is set - this.CreateTooltipWindow(); - - // subscribe to display settings changes (so that we know when the screen resolution changes, so that we can reposition our button) - Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; - - // if the user is using Windows 11, create a mouse message hook (so we can capture the mousemove and click events over our taskbar icon) - if (Morphic.Windows.Native.OsVersion.OsVersion.IsWindows11OrLater() == true) - { - _mouseHook = new Windows.Native.WindowMessageHooks.MouseWindowMessageHook(); - _mouseHook.WndProcEvent += _mouseHook_WndProcEvent; - } - - // position the tray button in its initial position - // NOTE: the button has no icon at this point; if we want to move this logic to the Icon set routine, - // that's reasonable, but we'd need to think through any side-effects (and we'd need to do this here anyway - // if an icon had already been set prior to .Initialize being called) - //if (_iconHandle != IntPtr.Zero) - //{ - this.PositionTrayButton(); - //} - } - - // NOTE: this function is somewhat redundant and is provided to support Windows 11; we should refactor all of this code to handle window messages centrally - private void _mouseHook_WndProcEvent(object? sender, Windows.Native.WindowMessageHooks.MouseWindowMessageHook.WndProcEventArgs e) - { - // TODO: we should ensure that calls are queued and then called from a sequential thread (ideally a UI dispatch thread) - switch ((WinApi.WindowMessage)e.Message) - { - case WinApi.WindowMessage.WM_LBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - break; - case WinApi.WindowMessage.WM_LBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - { - var mouseArgs = new MouseEventArgs(MouseButtons.Left, 1, e.X, e.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - break; - case WinApi.WindowMessage.WM_MOUSELEAVE: - // the cursor has left our tray button's window area; remove the hover state from our visual state - _visualState &= ~TrayButtonVisualStateFlags.Hover; - // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here - // (and then we check them again when the mouse moves back over the button) - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - break; - case WinApi.WindowMessage.WM_MOUSEMOVE: - // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) - // - // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because - // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during - // mousemove so that we can re-enable the pressed state if/where appropriate. - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0)) - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - } - if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0)) - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - } - // - break; - case WinApi.WindowMessage.WM_RBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - break; - case WinApi.WindowMessage.WM_RBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - { - var mouseArgs = new MouseEventArgs(MouseButtons.Right, 1, e.X, e.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - break; - } - } - - internal void SetText(string? text) - { - _tooltipText = text; - this.UpdateTooltipTextAndTracking(); - } - - private void CreateTooltipWindow() - { - if (_tooltipWindowHandle != IntPtr.Zero) - { - // tooltip window already exists - return; - } - - _tooltipWindowHandle = WinApi.CreateWindowEx( - 0 /* no styles */, - WinApi.TOOLTIPS_CLASS, - null, - WinApi.WindowStyles.WS_POPUP | (WinApi.WindowStyles)WinApi.TTS_ALWAYSTIP, - WinApi.CW_USEDEFAULT, - WinApi.CW_USEDEFAULT, - WinApi.CW_USEDEFAULT, - WinApi.CW_USEDEFAULT, - this.Handle, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (_tooltipWindowHandle == IntPtr.Zero) - { - Debug.Assert(false, "Could not create tooltip window"); - } - - this.UpdateTooltipTextAndTracking(); - } - - private void DestroyTooltipWindow() - { - // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. - _tooltipText = null; - this.UpdateTooltipTextAndTracking(); - - WinApi.DestroyWindow(_tooltipWindowHandle); - _tooltipWindowHandle = IntPtr.Zero; - } - - private void UpdateTooltipTextAndTracking() - { - if (_tooltipWindowHandle == IntPtr.Zero) - { - // tooltip window does not exist; failed; abort - Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); - return; - } - - WinApi.RECT trayButtonClientRect; - var getClientRectSuccess = WinApi.GetClientRect(this.Handle, out trayButtonClientRect); - if (getClientRectSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); - return; - } - - var toolinfo = new WinApi.TOOLINFO(); - toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); - toolinfo.hwnd = this.Handle; - toolinfo.uFlags = WinApi.TTF_SUBCLASS; - toolinfo.lpszText = _tooltipText; - toolinfo.uId = this.Handle; // unique identifier (for adding/deleting the tooltip) - toolinfo.rect = trayButtonClientRect; - // - var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); - try - { - Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); - if (toolinfo.lpszText is not null) - { - if (_tooltipInfoAdded == false) - { - _ = WinApi.SendMessage(_tooltipWindowHandle, (int)WinApi.TTM_ADDTOOL, 0, pointerToToolinfo); - _tooltipInfoAdded = true; - } - else - { - // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) - _ = WinApi.SendMessage(_tooltipWindowHandle, (int)WinApi.TTM_DELTOOL, 0, pointerToToolinfo); - _ = WinApi.SendMessage(_tooltipWindowHandle, (int)WinApi.TTM_ADDTOOL, 0, pointerToToolinfo); - } - } - else - { - // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed - _ = WinApi.SendMessage(_tooltipWindowHandle, (int)WinApi.TTM_DELTOOL, 0, pointerToToolinfo); - _tooltipInfoAdded = false; - } - } - finally - { - Marshal.FreeHGlobal(pointerToToolinfo); - } - } - - // NOTE: intial creation events are captured by this callback, but afterwards window messages are captured by WndProc instead - private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - switch ((WinApi.WindowMessage)msg) { - case WinApi.WindowMessage.WM_CREATE: - if (WinApi.BufferedPaintInit() != WinApi.S_OK) - { - // failed; abort - Debug.Assert(false, "Could not initialize buffered paint"); - return new IntPtr(-1); // abort window creation process - } - break; - default: - break; - } - - // pass all non-handled messages through to DefWindowProc - return WinApi.DefWindowProc(hWnd, msg, wParam, lParam); - } - - // NOTE: the built-in CreateHandle function couldn't handle our custom class, so we have overridden CreateHandle and are calling CreateWindowEx ourselves - public override void CreateHandle(CreateParams cp) - { - // NOTE: if cp.ClassName is a string parseable as a (UInt16) number, convert that value to an IntPtr; otherwise capture a pointer to the string - IntPtr classNameAsIntPtr; - bool mustReleaseClassNameAsIntPtr = false; - // - ushort classNameAsUInt16 = 0; - if (ushort.TryParse(cp.ClassName, out classNameAsUInt16) == true) - { - classNameAsIntPtr = (IntPtr)classNameAsUInt16; - mustReleaseClassNameAsIntPtr = false; - } - else - { - classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); - mustReleaseClassNameAsIntPtr = true; - } - - // TODO: in some circumstances, it is possible that we are unable to create our window; consider creating a retry mechanism (dealing with async) or notify our caller - try - { - var handle = WinApi.CreateWindowEx( - (WinApi.WindowStylesEx)cp.ExStyle, - (IntPtr)Int64.Parse(cp.ClassName), - cp.Caption, - (WinApi.WindowStyles)cp.Style, - cp.X, - cp.Y, - cp.Width, - cp.Height, - cp.Parent, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - - // NOTE: in our testing, handle was sometimes IntPtr.Zero here (in which case the tray icon button's window will not exist) - if (handle == IntPtr.Zero) - { - Debug.Assert(false, "Could not create tray button window handle"); - } - - this.AssignHandle(handle); - } - finally - { - if (mustReleaseClassNameAsIntPtr == true) - { - Marshal.Release(classNameAsIntPtr); - } - } - } - - public void Dispose() - { - // TODO: if we are the topmost/leftmost next-to-tray-icon button, we should expand the task button container so it takes up our now-unoccupied space - - if (_mouseHook is not null) - { - _mouseHook.Dispose(); - } - - Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; - - this.DestroyTooltipWindow(); - this.DestroyHandle(); - } - - protected override void WndProc(ref Message m) - { - var uMsg = (uint)m.Msg; - - IntPtr? result = null; - - switch ((WinApi.WindowMessage)uMsg) - { - case WinApi.WindowMessage.WM_DESTROY: - /* TODO: trace to see if WM_DESTROY is actually called here; if not, then we should place the uninit in dispose instead; we might also consider - * not using BufferedPaintInit/UnInit at all (although that _might_ slow down our buffered painting execution a tiny bit) */ - WinApi.BufferedPaintUnInit(); - break; - case WinApi.WindowMessage.WM_DISPLAYCHANGE: - // screen resolution has changed: reposition the tray button - // NOTE: m.wParam contains bit depth - // NOTE: m.lParam contains the resolutions of the screen (horizontal resolution in low-order word; vertical resolution in high-order word) - this.PositionTrayButton(); - break; - case WinApi.WindowMessage.WM_ERASEBKGND: - // we will handle erasing the background, so return a non-zero value here - result = new IntPtr(1); - break; - case WinApi.WindowMessage.WM_LBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - { - var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (hitPoint is null) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - break; - } - var mouseArgs = new MouseEventArgs(MouseButtons.Left, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_MOUSEACTIVATE: - // do not activate our window (and discard this message) - result = new IntPtr(WinApi.MA_NOACTIVATEANDEAT); - break; - case WinApi.WindowMessage.WM_MOUSELEAVE: - // the cursor has left our tray button's window area; remove the hover state from our visual state - _visualState &= ~TrayButtonVisualStateFlags.Hover; - // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here - // (and then we check them again when the mouse moves back over the button) - _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_MOUSEMOVE: - // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) - // - // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because - // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during - // mousemove so that we can re-enable the pressed state if/where appropriate. - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & WinApi.MK_LBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - } - if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & WinApi.MK_RBUTTON) != 0)) - { - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - } - // - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_NCHITTEST: - var hitTestX = (short)((m.LParam.ToInt64() >> 0) & 0xFFFF); - var hitTestY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF); - // - WinApi.RECT trayButtonRectInScreenCoordinates; - if (WinApi.GetWindowRect(this.Handle, out trayButtonRectInScreenCoordinates) == false) - { - // fail; abort - Debug.Assert(false, "Could not get rect of tray button in screen coordinates"); - return; - } - // - if ((hitTestX >= trayButtonRectInScreenCoordinates.Left) && (hitTestX < trayButtonRectInScreenCoordinates.Right) && - (hitTestY >= trayButtonRectInScreenCoordinates.Top) && (hitTestY < trayButtonRectInScreenCoordinates.Bottom)) - { - // inside client area - result = new IntPtr(1); // HTCLIENT - } - else - { - // nowhere - // TODO: determine if there is another response we should be returning instead; the documentation is not clear in this regard - result = new IntPtr(0); // HTNOWHERE - } - break; - case WinApi.WindowMessage.WM_NCPAINT: - // no non-client (frame) area to paint - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_PAINT: - this.Paint(m.HWnd); - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_RBUTTONUP: - _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - { - var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); - if (hitPoint is null) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - break; - } - var mouseArgs = new MouseEventArgs(MouseButtons.Right, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); - _owner.MouseUp?.Invoke(_owner, mouseArgs); - } - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_SETCURSOR: - // wParam: window handle - // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event - var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); - var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); - switch ((WinApi.WindowMessage)mouseMsg) - { - case WinApi.WindowMessage.WM_LBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; - this.RequestRedraw(); - result = new IntPtr(1); - break; - case WinApi.WindowMessage.WM_LBUTTONUP: - result = new IntPtr(1); - break; - case WinApi.WindowMessage.WM_MOUSEMOVE: - // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter") then do so now - if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) - { - // track mousehover (for tooltips) and mouseleave (to remove hover effect) - var eventTrack = new WinApi.TRACKMOUSEEVENT(WinApi.TMEFlags.TME_LEAVE, this.Handle, WinApi.HOVER_DEFAULT); - var trackMouseEventSuccess = WinApi.TrackMouseEvent(ref eventTrack); - if (trackMouseEventSuccess == false) - { - // failed - Debug.Assert(false, "Could not set up tracking of tray button window area"); - return; - } - - _visualState |= TrayButtonVisualStateFlags.Hover; - - this.RequestRedraw(); - } - result = new IntPtr(1); - break; - case WinApi.WindowMessage.WM_RBUTTONDOWN: - _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; - this.RequestRedraw(); - result = new IntPtr(1); - break; - case WinApi.WindowMessage.WM_RBUTTONUP: - result = new IntPtr(1); - break; - default: - //Debug.WriteLine("UNHANDLED SETCURSOR Mouse Message: " + mouseMsg.ToString()); - break; - } - break; - case WinApi.WindowMessage.WM_SIZE: - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_WINDOWPOSCHANGED: - result = new IntPtr(0); - break; - case WinApi.WindowMessage.WM_WINDOWPOSCHANGING: - // in this implementation, we don't do anything with this message; nothing to do here - result = new IntPtr(0); - break; - default: - // unhandled message; this will be passed onto DefWindowProc instead - break; - } - - if (result.HasValue == true) - { - m.Result = result.Value; - } - else - { - m.Result = WinApi.DefWindowProc(m.HWnd, (uint)m.Msg, m.WParam, m.LParam); - } - } - - private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) - { - // start a timer which will verify that the button is positioned properly (and will give up after a certain number of attempts) - var checkupInterval = new TimeSpan(0, 0, 0, 0, 250); - _trayButtonPositionCheckupTimerCounter = 40; // count down for 10 seconds (0.250 x 40) - _trayButtonPositionCheckupTimer = new System.Threading.Timer(TrayButtonPositionCheckup, null, checkupInterval, checkupInterval); - } - private void TrayButtonPositionCheckup(object? state) - { - if (_trayButtonPositionCheckupTimerCounter <= 0) - { - _trayButtonPositionCheckupTimer?.Dispose(); - _trayButtonPositionCheckupTimer = null; - return; - } - // - _trayButtonPositionCheckupTimerCounter = Math.Max(_trayButtonPositionCheckupTimerCounter - 1, 0); - - // check the current and desired positions of the notify tray icon - var calculateResult = this.CalculateCurrentAndTargetRectOfTrayButton(); - if (calculateResult is not null) - { - if (calculateResult.Value.changeToRect is not null) - { - this.PositionTrayButton(); - } - } - } - - private WinApi.POINT? ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) - { - var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); - var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); - // convert x and y to screen coordinates - var hitPoint = new WinApi.POINT(x, y); - var mapWindowPointsResult = WinApi.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); - if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != WinApi.ERROR_SUCCESS) - { - // failed; abort - Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); - return null; - } - - return hitPoint; - } - private void Paint(IntPtr hWnd) - { - WinApi.PAINTSTRUCT ps = new WinApi.PAINTSTRUCT(); - IntPtr paintDc = WinApi.BeginPaint(hWnd, out ps); - try - { - IntPtr bufferedPaintDc; - // NOTE: ps.rcPaint was an empty rect in our intiail tests, so we are using a manually-created clientRect (from GetClientRect) here instead - var paintBufferHandle = WinApi.BeginBufferedPaint(ps.hdc, ref ps.rcPaint, WinApi.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, IntPtr.Zero, out bufferedPaintDc); - try - { - if (ps.rcPaint == WinApi.RECT.Empty) - { - // no rectangle; nothing to do - return; - } - - // clear our buffer background (to ARGB(0,0,0,0)) - var bufferedPaintClearSuccess = WinApi.BufferedPaintClear(paintBufferHandle, ref ps.rcPaint); - if (bufferedPaintClearSuccess != WinApi.S_OK) - { - // failed; abort - Debug.Assert(false, "Could not clear tray button's background"); - return; - } - - // if the user has pressed (mousedown) on our tray button or is hovering over it, highlight the tray button now - Double highlightOpacity = 0.0; - if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || - ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) - { - highlightOpacity = 0.25; - } - else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) - { - highlightOpacity = 0.1; - } - // - if (highlightOpacity > 0.0) - { - this.DrawHighlightBackground(bufferedPaintDc, ps.rcPaint, Color.White, highlightOpacity); - } - - // calculate the size and position of our icon - int iconWidthAndHeight = this.CalculateWidthAndHeightForIcon(ps.rcPaint); - // - var xLeft = ((ps.rcPaint.Right - ps.rcPaint.Left) - iconWidthAndHeight) / 2; - var yTop = ((ps.rcPaint.Bottom - ps.rcPaint.Top) - iconWidthAndHeight) / 2; - - if (_iconHandle != IntPtr.Zero && iconWidthAndHeight > 0) - { - var drawIconSuccess = WinApi.DrawIconEx(bufferedPaintDc, xLeft, yTop, _iconHandle, iconWidthAndHeight, iconWidthAndHeight, 0 /* not animated */, IntPtr.Zero /* no triple-buffering */, WinApi.DrawIconFlags.DI_NORMAL); - if (drawIconSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not draw tray button's icon"); - return; - } - } - } - finally - { - WinApi.EndBufferedPaint(paintBufferHandle, true); - } - } - finally - { - WinApi.EndPaint(hWnd, ref ps); - } - } - - private int CalculateWidthAndHeightForIcon(WinApi.RECT rect) - { - int result; - // NOTE: we currently measure the size of our icon by measuring the size of the rectangle - // NOTE: we use the larger of the two dimensions (height vs width) to determine our icon size; we may reconsider this in the future if we support non-square icons - int largerDimensionLenth; - if (rect.Bottom - rect.Top > rect.Right - rect.Left) - { - largerDimensionLenth = rect.Bottom - rect.Top; - } - else - { - largerDimensionLenth = rect.Right - rect.Left; - } - // - if (largerDimensionLenth >= 48) - { - result = 32; - } - else if (largerDimensionLenth >= 36) - { - result = 24; - } - else if (largerDimensionLenth >= 30) - { - result = 20; - } - else if (largerDimensionLenth >= 24) - { - result = 16; - } - else - { - result = 0; - } - - return result; - } - - private void DrawHighlightBackground(IntPtr hdc, WinApi.RECT rect, Color color, Double opacity) - { - // GDI doesn't have a concept of semi-transparent pixels - the only function that honours them is AlphaBlend. - // Create a bitmap containing a single pixel - and then use AlphaBlend to stretch it to the size of the rect. - - // set up the 1x1 pixel bitmap's configuration - var pixelBitmapInfo = new WinApi.BITMAPINFO(); - pixelBitmapInfo.bmiHeader = new WinApi.BITMAPINFOHEADER() - { - biWidth = 1, - biHeight = 1, - biPlanes = 1, // must be 1 - biBitCount = 32, // maximum of 2^32 colors - biCompression = WinApi.BitmapCompressionType.BI_RGB, - biSizeImage = 0, - biClrUsed = 0, - biClrImportant = 0 - }; - pixelBitmapInfo.bmiHeader.biSize = (uint)Marshal.SizeOf(pixelBitmapInfo.bmiHeader); - pixelBitmapInfo.bmiColors = new WinApi.RGBQUAD[1]; - - // calculate the pixel color as a uint32 (in AARRGGBB order) - uint pixelColor = ( - (((uint)color.A) << 24) | // NOTE: we ignore the alpha value in our call to AlphaBlend - (((uint)color.R) << 16) | - (((uint)color.G) << 8) | - (((uint)color.B) << 0)); - - // create the memory device context for the pixel - var pixelDc = WinApi.CreateCompatibleDC(hdc); - if (pixelDc == IntPtr.Zero) - { - // failed; abort - Debug.Assert(false, "Could not create device context for highlight pixel."); - return; - } - try - { - IntPtr pixelDibBitValues; - var pixelDibHandle = WinApi.CreateDIBSection(pixelDc, ref pixelBitmapInfo, WinApi.DIB_RGB_COLORS, out pixelDibBitValues, IntPtr.Zero, 0); - if (pixelDibHandle == IntPtr.Zero) - { - // failed; abort - Debug.Assert(false, "Could not create DIB for highlight pixel."); - return; - } - // - try - { - var selectedBitmapHandle = WinApi.SelectObject(pixelDc, pixelDibHandle); - if (selectedBitmapHandle == IntPtr.Zero) - { - // failed; abort - Debug.Assert(false, "Could not select object into the pixel device context."); - return; - } - try - { - // write over the single pixel's value (with the passed-in pixel) - Marshal.WriteIntPtr(pixelDibBitValues, new IntPtr(pixelColor)); - - // draw the highlight (stretching the pixel to the full rectangle size) - WinApi.BLENDFUNCTION blendFunction = new WinApi.BLENDFUNCTION() - { - BlendOp = (byte)WinApi.AC_SRC_OVER, - BlendFlags = 0, // must be zero - SourceConstantAlpha = (byte)(opacity * 255), // the requested opacity level - AlphaFormat = 0 - }; - var RESULT_TO_USE = WinApi.AlphaBlend(hdc, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, pixelDc, 0, 0, 1, 1, blendFunction); - } - finally - { - _ = WinApi.SelectObject(pixelDc, selectedBitmapHandle); - } - } - finally - { - _ = WinApi.DeleteObject(pixelDibHandle); - } - } - finally - { - _ = WinApi.DeleteDC(pixelDc); - } - } - - public void SetIcon(Icon? icon) - { - if (icon is not null) - { - _iconHandle = icon.Handle; - } - else - { - _iconHandle = IntPtr.Zero; - } - - // TODO: if we support non-square icons, then reposition the tray button based on the new dimensions of the icon (in case it's wider/narrower) - //this.PositionTrayButton(); - - // trigger a redraw - this.RequestRedraw(); - } - - private void PositionTrayButton() - { - var trayButtonRects = CalculateCurrentAndTargetRectOfTrayButton(); - if (trayButtonRects is null) - { - // fail; abort - Debug.Assert(false, "Could not calculate current and/or new rects for tray button"); - return; - } - // - var currentRect = trayButtonRects.Value.currentRect; - var changeToRect = trayButtonRects.Value.changeToRect; - var taskbarOrientation = trayButtonRects.Value.orientation; - - if (_mouseHook is not null) - { - // update our tracking region to track the new position (unless we haven't moved, in which case continue to track our current position) - if (changeToRect is not null) - { - _mouseHook.UpdateTrackingRegion(changeToRect.Value.ToPInvokeRect()); - } - else if (currentRect is not null) - { - _mouseHook.UpdateTrackingRegion(currentRect.Value.ToPInvokeRect()); - } - else - { - Debug.Assert(false, "Could not determine current RECT of tray button"); - } - } - - // if changeToRect is more leftmost/topmost than the task button container's right side, then shrink the task button container appropriately - WinApi.RECT? newTaskButtonContainerRect = null; - if (changeToRect is not null) - { - var taskbarTripletHandles = this.GetTaskbarTripletHandles(); - var taskbarTripletRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); - if (taskbarTripletRects is null) - { - // failed; abort - Debug.Assert(false, "could not get rects of taskbar or its important children"); - return; - } - var taskButtonContainerRect = taskbarTripletRects.Value.TaskButtonContainerRect; - - if ((taskbarOrientation == Orientation.Horizontal) && (taskButtonContainerRect.Right > changeToRect.Value.Left)) - { - newTaskButtonContainerRect = new WinApi.RECT(new System.Windows.Rect( - taskButtonContainerRect.Left, - taskButtonContainerRect.Top, - Math.Max(taskButtonContainerRect.Right - taskButtonContainerRect.Left - (taskButtonContainerRect.Right - changeToRect.Value.Left), 0), - taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - )); - } - else if ((taskbarOrientation == Orientation.Vertical) && taskButtonContainerRect.Bottom > changeToRect.Value.Top) - { - newTaskButtonContainerRect = new WinApi.RECT(new System.Windows.Rect( - taskButtonContainerRect.Left, - taskButtonContainerRect.Top, - taskButtonContainerRect.Right - taskButtonContainerRect.Left, - taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - Math.Max(taskButtonContainerRect.Bottom - changeToRect.Value.Top, 0) - )); - } - } - // - if (newTaskButtonContainerRect is not null) - { - var taskButtonContainerHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); - - // shrink the task button container - // NOTE: this is a blocking call, waiting until the task button container is resized; we do this intentionally so that we see its updated size synchronously - var repositionTaskButtonContainerSuccess = WinApi.SetWindowPos( - taskButtonContainerHandle, - IntPtr.Zero, - newTaskButtonContainerRect.Value.Left, - newTaskButtonContainerRect.Value.Top, - newTaskButtonContainerRect.Value.Right - newTaskButtonContainerRect.Value.Left, - newTaskButtonContainerRect.Value.Bottom - newTaskButtonContainerRect.Value.Top, - WinApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | - WinApi.SetWindowPosFlags.SWP_NOMOVE /* retain the current x and y position, out of an abundance of caution */ | - WinApi.SetWindowPosFlags.SWP_NOZORDER /* retain the current Z order (ignoring the hWndInsertAfter parameter) */ - ); - - if (repositionTaskButtonContainerSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not resize taskbar's task button container"); - return; - } - } - - // if our button needs to move (either because we don't know the old RECT or because the new RECT is different), do so now - if (changeToRect is not null) - { - if (currentRect.HasValue == false || (currentRect.Value != changeToRect.Value)) - { - var taskbarHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarHandle(); - - // convert our tray button's position from desktop coordinates to "child" coordinates within the taskbar - WinApi.RECT childRect = changeToRect.Value; - var mapWindowPointsResult = WinApi.MapWindowPoints(IntPtr.Zero /* use screen coordinates */, taskbarHandle, ref childRect, 2 /* 2 indicates that lpPoints is a RECT */); - if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != WinApi.ERROR_SUCCESS) - { - // failed; abort - Debug.Assert(false, "Could not map tray button RECT points to taskbar window handle"); - return; - } - - var repositionTrayButtonSuccess = WinApi.SetWindowPos( - this.Handle, - WinApi.HWND_TOP, - childRect.Left, - childRect.Top, - childRect.Right - childRect.Left, - childRect.Bottom - childRect.Top, - WinApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | - WinApi.SetWindowPosFlags.SWP_SHOWWINDOW /* display the tray button */ - ); - - if (repositionTrayButtonSuccess == false) - { - // failed; abort - Debug.Assert(false, "Could not reposition and/or resize tray button"); - return; - } - } - - // as we have moved/resized, request a repaint - this.RequestRedraw(); - - // if we have tooltip text, update its tracking rectangle - if (_tooltipText is not null) - { - UpdateTooltipTextAndTracking(); - } - } - } - - private (IntPtr TaskbarHandle, IntPtr TaskButtonContainerHandle, IntPtr NotifyTrayHandle) GetTaskbarTripletHandles() - { - var taskbarHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarHandle(); - var taskButtonContainerHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); - var notifyTrayHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarNotificationTrayHandle(); - - return (taskbarHandle, taskButtonContainerHandle, notifyTrayHandle); - } - - private (WinApi.RECT TaskbarRect, WinApi.RECT TaskButtonContainerRect, WinApi.RECT NotifyTrayRect)? GetTaskbarTripletRects(IntPtr taskbarHandle, IntPtr taskButtonContainerHandle, IntPtr notifyTrayHandle) - { - // find the taskbar and its rect - WinApi.RECT taskbarRect = new WinApi.RECT(); - if (WinApi.GetWindowRect(taskbarHandle, out taskbarRect) == false) - { - // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar."); - return null; - } - - // find the window handles and rects of the task button container and the notify tray (which are children inside of the taskbar) - // - WinApi.RECT taskButtonContainerRect = new WinApi.RECT(); - if (WinApi.GetWindowRect(taskButtonContainerHandle, out taskButtonContainerRect) == false) - { - // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar's task button list container."); - return null; - } - // - WinApi.RECT notifyTrayRect = new WinApi.RECT(); - if (WinApi.GetWindowRect(notifyTrayHandle, out notifyTrayRect) == false) - { - // failed; abort - Debug.Assert(false, "Could not obtain window handle to taskbar's notify tray."); - return null; - } - - return (taskbarRect, taskButtonContainerRect, notifyTrayRect); - } - - private (WinApi.RECT availableAreaRect, List childRects) CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(IntPtr taskbarHandle, Orientation taskbarOrientation, WinApi.RECT taskbarRect, WinApi.RECT taskButtonContainerRect, WinApi.RECT notifyTrayRect) - { - // calculate the total "free area" rectangle (the area between the task button container and the notify tray where we want to place our tray button) - WinApi.RECT freeAreaAvailableRect; - if (taskbarOrientation == Orientation.Horizontal) - { - freeAreaAvailableRect = new WinApi.RECT(new System.Windows.Rect(taskButtonContainerRect.Right, taskbarRect.Top, Math.Max(notifyTrayRect.Left - taskButtonContainerRect.Right, 0), Math.Max(taskbarRect.Bottom - taskbarRect.Top, 0))); - } - else - { - freeAreaAvailableRect = new WinApi.RECT(new System.Windows.Rect(taskbarRect.Left, taskButtonContainerRect.Bottom, Math.Max(taskbarRect.Right - taskbarRect.Left, 0), Math.Max(notifyTrayRect.Top - taskButtonContainerRect.Bottom, 0))); - } - - // capture a list of all child windows within the taskbar; we'll use this list to enumerate the rects of all the taskbar's children - var taskbarChildHandles = MorphicTrayButtonNativeWindow.EnumerateChildWindows(taskbarHandle); - // - // find the rects of all windows within the taskbar; we need this information so that we do not overlap any other accessory windows which are trying to sit in the same area as us - var taskbarChildHandlesWithRects = new Dictionary(); - foreach (var taskbarChildHandle in taskbarChildHandles) - { - WinApi.RECT taskbarChildRect = new WinApi.RECT(); - if (WinApi.GetWindowRect(taskbarChildHandle, out taskbarChildRect) == true) - { - taskbarChildHandlesWithRects.Add(taskbarChildHandle, taskbarChildRect); - } - else - { - Debug.Assert(false, "Could not capture RECTs of all taskbar child windows"); - } - } - - // remove any child rects which are contained inside the task button container (so that we eliminate any subchildren from our calculations) - foreach (var taskbarChildHandle in taskbarChildHandles) - { - if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) - { - var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; - if (taskbarChildRect.IsInside(taskButtonContainerRect)) - { - taskbarChildHandlesWithRects.Remove(taskbarChildHandle); - } - } - } - - // remove our own (tray button) window handle from the list (so that we don't see our current screen rect as "taken" in the list of occupied RECTs) - taskbarChildHandlesWithRects.Remove(this.Handle); - - // create a list of children which are located between the task button container and the notify tray (i.e. windows which are occupying the same region we want to - // occupy...so we can try to avoid overlapping) - List freeAreaChildRects = new List(); - foreach (var taskbarChildHandle in taskbarChildHandles) - { - if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) - { - var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; - if ((taskbarChildRect.IsInside(freeAreaAvailableRect) == true) && - (taskbarChildRect.HasNonZeroWidthOrHeight() == false)) - { - freeAreaChildRects.Add(taskbarChildRect); - } - } - } - - return (freeAreaAvailableRect, freeAreaChildRects); - } - - // NOTE: this function returns a newPosition IF the tray button should be moved - private (WinApi.RECT? currentRect, WinApi.RECT? changeToRect, Orientation orientation)? CalculateCurrentAndTargetRectOfTrayButton() - { - // NOTE: there are scenarios we must deal with where there may be multiple potential "taskbar button" icons to the left of the notification tray; in those scenarios, we must: - // 1. Position ourself to the left of the other icon-button(s) (or in an empty space in between them) - // 2. Reposition our icon when the other icon-button(s) are removed from the taskbar (e.g. when their host applications close them) - // 3. If we detect that we and another application are writing on top of each other (or repositioning the taskbar button container on top of our icon), then we must fail - // gracefully and let our host application know so it can warn the user, place the icon in the notification tray instead, etc. - - // To position the tray button, we need to find three windows: - // 1. the taskbar itself - // 2. the section of the taskbar which holds the taskbar buttons (i.e. to the right of the start button and find/cortana/taskview buttons, but to the left of the notification tray) */ - // 3. the notification tray - // - // We will then resize the section of the taskbar that holds the taskbar buttons so that we can place our tray button to its right (i.e. to the left of the notification tray). - - var taskbarTripletHandles = this.GetTaskbarTripletHandles(); - var taskbarHandle = taskbarTripletHandles.TaskbarHandle; - - var taskbarRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); - if (taskbarRects is null) - { - return null; - } - var taskbarRect = taskbarRects.Value.TaskbarRect; - var taskButtonContainerRect = taskbarRects.Value.TaskButtonContainerRect; - var notifyTrayRect = taskbarRects.Value.NotifyTrayRect; - - // determine the taskbar's orientation - System.Windows.Forms.Orientation taskbarOrientation; - if ((taskbarRect.Right - taskbarRect.Left) > (taskbarRect.Bottom - taskbarRect.Top)) - { - taskbarOrientation = Orientation.Horizontal; - } - else - { - taskbarOrientation = Orientation.Vertical; - } - - // calculate all of the free rects between the task button container and notify tray - var calculateEmptyRectsResult = this.CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(taskbarHandle, taskbarOrientation, taskbarRect, taskButtonContainerRect, notifyTrayRect); - var freeAreaChildRects = calculateEmptyRectsResult.childRects; - var freeAreaAvailableRect = calculateEmptyRectsResult.availableAreaRect; - - /* determine the rect for our tray button; based on our current positioning strategy, this will either be its existing position or the leftmost/topmost "next to tray" position. - * If we are determining the leftmost/topmost "next to tray" position, we will find the available space between the task button container and the notification tray (or any - * already-present controls that are already left/top of the notification tray); if there is not enough free space available in that area then we will shrink the task button - * container to make room. */ - // - /* NOTE: there are some deficiencies to our current positioning strategy. Of note... - * 1. In some circumstances, it might be possible that we are leaving "holes" of available space between the task button container and the notification tray; but if that - * happens, it might be something beyond our control (as other apps may have created that space). One concern is if we shrink our icon (in which case we should in theory - * shrink the space to our top/left) - * 2. If other apps draw their next-to-tray buttons after us and are not watching for conflicts then they could draw over us; a mitigation measure in that instance might be to - * use a timer to check that our tray button is not obscured and then remedy the situation; if we got into a "fight" over real estate that appeared to never terminate then - * we could destroy our icon and raise an event letting the application know it should choose an alternate strategy (such as a notification tray icon) instead. - * 3. If a more-rightmost/bottommost icon's application is closed while we are running, the taskbar could be resized to obscure us; we might need a timer (or we might need to - * capture the appropriate window message) to discover this scenario. - * In summary there is no standardized system (other than perhaps the "(dock) toolbar in taskbar" mechanism); if we find that we encounter problems in the field with our current - * strategy, we may want to consider rebuilding this functionality via the "toolbar in taskbar" mechanism. See HP Support Assistant for an example of another application - * which is doing what we are trying to do with the next-to-tray button strategy */ - - // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) - int trayButtonHeight; - int trayButtonWidth; - if (taskbarOrientation == Orientation.Horizontal) - { - trayButtonHeight = taskbarRect.Bottom - taskbarRect.Top; - trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); - } - else - { - trayButtonWidth = taskbarRect.Right - taskbarRect.Left; - trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); - } - - // get our current rect (in case we can just reuse the current position...and also to make sure it doesn't need to be resized) - WinApi.RECT currentRectAsNonNullable; - WinApi.RECT? currentRect = null; - WinApi.RECT? currentRectForResult = null; - if (WinApi.GetWindowRect(this.Handle, out currentRectAsNonNullable) == true) - { - currentRect = currentRectAsNonNullable; - currentRectForResult = currentRectAsNonNullable; - } - - // if the current position of our window isn't the right size for our icon, then set it to NULL so we don't try to reuse it. - if ((currentRect is not null) && - ((currentRect.Value.Right - currentRect.Value.Left != trayButtonWidth) || (currentRect.Value.Bottom - currentRect.Value.Top != trayButtonHeight))) - { - currentRect = null; - } - - // calculate the new rect for our tray button's window - WinApi.RECT? newRect = null; - - // if the space occupied by our already-existing rect is not overlapped by anyone else and is in the free area, keep using the same space - if ((currentRect is not null) && (currentRect.Value.Intersects(freeAreaAvailableRect) == true)) - { - // by default, assume that our currentRect is still available (i.e. not overlapped) - bool currentRectIsNotOverlapped = true; - - // make sure we do not overlap another control in the free area - foreach (var freeAreaChildRect in freeAreaChildRects) - { - if (currentRect.Value.Intersects(freeAreaChildRect) == true) - { - // overlap conflict - currentRectIsNotOverlapped = false; - break; - } - } - - if (currentRectIsNotOverlapped == true) - { - // set "newRect" (the variable for where we will now place our tray button) to the same position we were already at - newRect = currentRect; - } - } - - // if our current (already-used-by-us) rect was not available, choose the leftmost/topmost space available - if (newRect is null) - { - if (taskbarOrientation == Orientation.Horizontal) - { - // horizontal taskbar: find the leftmost rect in the available space (which we'll then carve the "rightmost" section out of) - WinApi.RECT leftmostRect = freeAreaAvailableRect; - - foreach (var freeAreaChildRect in freeAreaChildRects) - { - if (freeAreaChildRect.Left < leftmostRect.Right) - { - leftmostRect.Right = freeAreaChildRect.Left; - } - } - - // choose the rightmost space in the leftmostRect area; expand our tray button towards the left if/as necessary - newRect = new WinApi.RECT(new System.Windows.Rect(leftmostRect.Right - trayButtonWidth, leftmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); - } - else - { - // vertical taskbar: find the topmost rect in the available space (which we'll then carve the "bottommost" section out of) - WinApi.RECT topmostRect = freeAreaAvailableRect; - - foreach (var freeAreaChildRect in freeAreaChildRects) - { - if (freeAreaChildRect.Top < topmostRect.Bottom) - { - topmostRect.Bottom = freeAreaChildRect.Top; - } - } - - // choose the bottommost space in the topmostRect area; expand our tray button towards the top if/as necessary - newRect = new WinApi.RECT(new System.Windows.Rect(topmostRect.Right - trayButtonWidth, topmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); - } - } - - WinApi.RECT? changeToRect = null; - if (newRect != currentRectForResult) - { - changeToRect = newRect; - } - - return (currentRectForResult, changeToRect, taskbarOrientation); - } - - private bool RequestRedraw() - { - return WinApi.RedrawWindow( - this.Handle, - IntPtr.Zero, - IntPtr.Zero, - WinApi.RedrawWindowFlags.RDW_ERASE | WinApi.RedrawWindowFlags.RDW_INVALIDATE | WinApi.RedrawWindowFlags.RDW_ALLCHILDREN - ); - } - - internal static List EnumerateChildWindows(IntPtr parentHwnd) - { - var result = new List(); - - // create an unmanaged pointer to our list (using a GC-managed handle) - GCHandle resultGCHandle = GCHandle.Alloc(result, GCHandleType.Normal); - // convert our GCHandle into an IntPtr (which we will unconvert back to a GCHandler in the EnumChildWindows callback) - IntPtr resultGCHandleAsIntPtr = GCHandle.ToIntPtr(resultGCHandle); - - try - { - var enumFunction = new WinApi.EnumWindowsProc(MorphicTrayButtonNativeWindow.EnumerateChildWindowsCallback); - WinApi.EnumChildWindows(parentHwnd, enumFunction, resultGCHandleAsIntPtr); - - } - finally - { - if (resultGCHandle.IsAllocated) - { - resultGCHandle.Free(); - } - } - - return result; - } - internal static bool EnumerateChildWindowsCallback(IntPtr hwnd, IntPtr lParam) - { - // convert lParam back into the result list object - var resultGCHandle = GCHandle.FromIntPtr(lParam); - List? result = resultGCHandle.Target as List; - - if (result is not null) - { - result.Add(hwnd); - } - else - { - Debug.Assert(false, "Could not enumerate child windows"); - } - - return true; - } - - internal static IntPtr FindWindowsTaskbarHandle() - { - return WinApi.FindWindow("Shell_TrayWnd", null); - } - - private static IntPtr FindWindowsTaskbarTaskButtonContainerHandle() - { - var taskbarHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) - { - return IntPtr.Zero; - } - return WinApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "ReBarWindow32", null); - } - - private static IntPtr FindWindowsTaskbarNotificationTrayHandle() - { - var taskbarHandle = MorphicTrayButtonNativeWindow.FindWindowsTaskbarHandle(); - if (taskbarHandle == IntPtr.Zero) { - return IntPtr.Zero; - } - return WinApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "TrayNotifyWnd", null); - } - - } - } - - #endregion -} diff --git a/Morphic.Client/MessageWatcherNativeWindow.cs b/Morphic.Client/MessageWatcherNativeWindow.cs deleted file mode 100644 index a460bbc5..00000000 --- a/Morphic.Client/MessageWatcherNativeWindow.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows.Forms; - -namespace Morphic.Client -{ - internal class MessageWatcherNativeWindow : NativeWindow, IDisposable - { - private List _watchedMessages; - - private const string NATIVE_WINDOW_CLASS_NAME = "Morphic-MessageWatcher"; - - public class WatchedMessageEventArgs : EventArgs - { - public uint Msg; - public IntPtr wParam; - public IntPtr lParam; - } - - public delegate void WatchedMessageReceived(object sender, WatchedMessageEventArgs args); - public event WatchedMessageReceived WatchedMessageEvent; - - internal MessageWatcherNativeWindow(List watchedMessages) - { - // capture the list of messages to watch; we do this one time at initialization to avoid any need for thread safety around this list - _watchedMessages = watchedMessages; - } - - public void Initialize() - { - // register our custom native window class - var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new WinApi.WndProc(this.WndProcCallback)); - var lpWndClass = new WinApi.WNDCLASSEX - { - cbSize = (uint)Marshal.SizeOf(typeof(WinApi.WNDCLASSEX)), - lpfnWndProc = pointerToWndProcCallback, - lpszClassName = NATIVE_WINDOW_CLASS_NAME, - hCursor = WinApi.LoadCursor(IntPtr.Zero, (int)WinApi.Cursors.IDC_ARROW) - }; - - var registerClassResult = WinApi.RegisterClassEx(ref lpWndClass); - if (registerClassResult == 0) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - - var windowParams = new CreateParams(); - windowParams.ExStyle = (int)WinApi.WindowStylesEx.WS_EX_NOACTIVATE; - /* NOTE: as we want to be able to ensure that we're referencing the exact class we just registered, we pass the RegisterClassEx results into the - * CreateWindow function (and we encode that result as a ushort here in a proprietary way) */ - windowParams.ClassName = registerClassResult.ToString(); // nativeWindowClassName; - //windowParams.Caption = nativeWindowClassName; - windowParams.Style = 0; - windowParams.X = 0; - windowParams.Y = 0; - windowParams.Width = 0; - windowParams.Height = 0; - windowParams.Parent = WinApi.HWND_MESSAGE; - // - // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException, or Win32Exception - this.CreateHandle(windowParams); - } - - // NOTE: intial creation events are captured by this callback, but afterwards window messages are captured by WndProc instead - private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - // NOTE: we do not need to handle any of the initial creation-time windows messages; if that ever changes, we can process them here - //switch ((WinApi.WindowMessage)msg) - //{ - // default: - // break; - //} - - // pass all non-handled messages through to DefWindowProc - return WinApi.DefWindowProc(hWnd, msg, wParam, lParam); - } - - // NOTE: the built-in CreateHandle function couldn't handle our custom class, so we have overridden CreateHandle and are calling CreateWindowEx ourselves - public override void CreateHandle(CreateParams cp) - { - // NOTE: if cp.ClassName is a string parseable as a (UInt16) number, convert that value to an IntPtr; otherwise capture a pointer to the string - IntPtr classNameAsIntPtr; - bool mustReleaseClassNameAsIntPtr = false; - // - ushort classNameAsUInt16 = 0; - if (ushort.TryParse(cp.ClassName, out classNameAsUInt16) == true) - { - classNameAsIntPtr = (IntPtr)classNameAsUInt16; - mustReleaseClassNameAsIntPtr = false; - } - else - { - classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); - mustReleaseClassNameAsIntPtr = true; - } - - try - { - var handle = WinApi.CreateWindowEx( - (WinApi.WindowStylesEx)cp.ExStyle, - (IntPtr)Int64.Parse(cp.ClassName), - cp.Caption, - (WinApi.WindowStyles)cp.Style, - cp.X, - cp.Y, - cp.Width, - cp.Height, - cp.Parent, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero - ); - - if (handle == IntPtr.Zero) - { - // if we could not create the handle, throw an exception - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - - this.AssignHandle(handle); - } - finally - { - if (mustReleaseClassNameAsIntPtr == true) - { - Marshal.Release(classNameAsIntPtr); - } - } - } - - public void Dispose() - { - this.DestroyHandle(); - } - - protected override void WndProc(ref Message m) - { - var uMsg = (uint)m.Msg; - - if (_watchedMessages.Contains(uMsg)) - { - var eventArgs = new WatchedMessageEventArgs(); - eventArgs.Msg = uMsg; - eventArgs.wParam = m.WParam; - eventArgs.lParam = m.LParam; - - this.WatchedMessageEvent?.Invoke(this, eventArgs); - } - - // pass the message through to he base handler (out of an abundance of caution; we could probably just leave m.Result as 0 instead) - m.Result = WinApi.DefWindowProc(m.HWnd, (uint)m.Msg, m.WParam, m.LParam); - } - - internal static void PostMessage(uint messageId, IntPtr wParam, IntPtr lParam) - { - // find the instance of our watch window; note that is designed to only find one instance - IntPtr watchWindowHandle = WinApi.FindWindow(NATIVE_WINDOW_CLASS_NAME, null); - - // send the message to the single instance watch window - WinApi.PostMessage(watchWindowHandle, messageId, wParam, lParam); - } - } -} diff --git a/Morphic.Client/Morphic.Client.csproj b/Morphic.Client/Morphic.Client.csproj deleted file mode 100644 index 9b559dbf..00000000 --- a/Morphic.Client/Morphic.Client.csproj +++ /dev/null @@ -1,170 +0,0 @@ - - - - WinExe - netcoreapp3.1 - enable - true - true - Icon.ico - - Morphic - false - AnyCPU;x64 - 1.5$(VersionBuildComponents) - localdev - $(VersionSuffix) - Morphic.Client.AppMain - Raising the Floor - Development - app.manifest - 9.0 - - - - - - - - - - - - - - - - - - PreserveNewest - - - - PreserveNewest - - - - - - Always - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - - - - Never - - - True - True - Resources.resx - - - True - True - UserSettings.settings - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - Never - - - Never - - - Never - - - SettingsSingleFileGenerator - UserSettings.Designer.cs - - - - - - Always - - - Always - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/Properties/Resources.Designer.cs b/Morphic.Client/Properties/Resources.Designer.cs deleted file mode 100644 index 3c12501b..00000000 --- a/Morphic.Client/Properties/Resources.Designer.cs +++ /dev/null @@ -1,775 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Morphic.Client.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Morphic.Client.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). - /// - internal static System.Drawing.Icon Icon { - get { - object obj = ResourceManager.GetObject("Icon", resourceCulture); - return ((System.Drawing.Icon)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.IO.UnmanagedMemoryStream similar to System.IO.MemoryStream. - /// - internal static System.IO.UnmanagedMemoryStream LoginAnnounce { - get { - return ResourceManager.GetStream("LoginAnnounce", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Customize buttons and take your settings with you anywhere. - /// - internal static string QuickStrip_Advanced_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Advanced_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Advanced Features. - /// - internal static string QuickStrip_Advanced_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Advanced_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Changes colors to adapt for color blindness (Right-click to choose type). - /// - internal static string QuickStrip_Colors_Color_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Colors_Color_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn color vision filters on and off. - /// - internal static string QuickStrip_Colors_Color_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Colors_Color_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color Vision. - /// - internal static string QuickStrip_Colors_Color_Name { - get { - return ResourceManager.GetString("QuickStrip_Colors_Color_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Color. - /// - internal static string QuickStrip_Colors_Color_Title { - get { - return ResourceManager.GetString("QuickStrip_Colors_Color_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Contrast on, off. - /// - internal static string QuickStrip_Colors_Contrast_Name { - get { - return ResourceManager.GetString("QuickStrip_Colors_Contrast_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Contrast. - /// - internal static string QuickStrip_Colors_Contrast_Title { - get { - return ResourceManager.GetString("QuickStrip_Colors_Contrast_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggles dark mode on and off (Right-click to adjust). - /// - internal static string QuickStrip_Colors_Dark_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Colors_Dark_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn Dark mode on and off. - /// - internal static string QuickStrip_Colors_Dark_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Colors_Dark_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dark mode. - /// - internal static string QuickStrip_Colors_Dark_Name { - get { - return ResourceManager.GetString("QuickStrip_Colors_Dark_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Dark. - /// - internal static string QuickStrip_Colors_Dark_Title { - get { - return ResourceManager.GetString("QuickStrip_Colors_Dark_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Night Light. - /// - internal static string QuickStrip_Colors_Night_Name { - get { - return ResourceManager.GetString("QuickStrip_Colors_Night_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Night. - /// - internal static string QuickStrip_Colors_Night_Title { - get { - return ResourceManager.GetString("QuickStrip_Colors_Night_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Contrast & color. - /// - internal static string QuickStrip_Colors_Title { - get { - return ResourceManager.GetString("QuickStrip_Colors_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Make it harder to distinguish items. - /// - internal static string QuickStrip_Contrast_Off_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Contrast_Off_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn Off High Contrast. - /// - internal static string QuickStrip_Contrast_Off_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Contrast_Off_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Off. - /// - internal static string QuickStrip_Contrast_Off_Title { - get { - return ResourceManager.GetString("QuickStrip_Contrast_Off_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Makes screen easier to read for some (Right-click to adjust contrast). - /// - internal static string QuickStrip_Contrast_On_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Contrast_On_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn Contrast on and off. - /// - internal static string QuickStrip_Contrast_On_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Contrast_On_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to On. - /// - internal static string QuickStrip_Contrast_On_Title { - get { - return ResourceManager.GetString("QuickStrip_Contrast_On_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to High Contrast. - /// - internal static string QuickStrip_Contrast_Title { - get { - return ResourceManager.GetString("QuickStrip_Contrast_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to (Right-click to change magnifier settings). - /// - internal static string QuickStrip_Magnifier_Hide_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Hide_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn off Magnifier. - /// - internal static string QuickStrip_Magnifier_Hide_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Hide_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hide Magnifier. - /// - internal static string QuickStrip_Magnifier_Hide_Name { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Hide_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hide. - /// - internal static string QuickStrip_Magnifier_Hide_Title { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Hide_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turns on a Magnifying Lens (Right-click to change settings). - /// - internal static string QuickStrip_Magnifier_Show_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Show_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn on Magnifier that Follows Mouse. - /// - internal static string QuickStrip_Magnifier_Show_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Show_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show Magnifier. - /// - internal static string QuickStrip_Magnifier_Show_Name { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Show_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show. - /// - internal static string QuickStrip_Magnifier_Show_Title { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Show_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Magnifier. - /// - internal static string QuickStrip_Magnifier_Title { - get { - return ResourceManager.GetString("QuickStrip_Magnifier_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Apply color suitable for the daytime.. - /// - internal static string QuickStrip_NightMode_Off_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_NightMode_Off_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn Off Night Mode. - /// - internal static string QuickStrip_NightMode_Off_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_NightMode_Off_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Off. - /// - internal static string QuickStrip_NightMode_Off_Title { - get { - return ResourceManager.GetString("QuickStrip_NightMode_Off_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Reduces eye strain and makes it easier to fall asleep at night. - /// - internal static string QuickStrip_NightMode_On_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_NightMode_On_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn 'Night Light' feature on and off. - /// - internal static string QuickStrip_NightMode_On_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_NightMode_On_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to On. - /// - internal static string QuickStrip_NightMode_On_Title { - get { - return ResourceManager.GetString("QuickStrip_NightMode_On_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Night mode. - /// - internal static string QuickStrip_NightMode_Title { - get { - return ResourceManager.GetString("QuickStrip_NightMode_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Select any text and then click to have it read. - /// - internal static string QuickStrip_Reader_Start_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Reader_Start_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Read the text that is selected. - /// - internal static string QuickStrip_Reader_Start_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Reader_Start_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Read Selected Text. - /// - internal static string QuickStrip_Reader_Start_Name { - get { - return ResourceManager.GetString("QuickStrip_Reader_Start_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to (Right-click to adjust speed and voice). - /// - internal static string QuickStrip_Reader_Stop_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Reader_Stop_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Stop Reading. - /// - internal static string QuickStrip_Reader_Stop_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Reader_Stop_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Stop Reading Select Text. - /// - internal static string QuickStrip_Reader_Stop_Name { - get { - return ResourceManager.GetString("QuickStrip_Reader_Stop_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Read Selected. - /// - internal static string QuickStrip_Reader_Title { - get { - return ResourceManager.GetString("QuickStrip_Reader_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Makes everything on screen larger. - /// - internal static string QuickStrip_Resolution_Bigger_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Bigger_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Increase Text Size Everywhere. - /// - internal static string QuickStrip_Resolution_Bigger_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Bigger_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The text and icons are as large as they can be. - /// - internal static string QuickStrip_Resolution_Bigger_LimitMessage { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Bigger_LimitMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot Increase Screen Zoom. - /// - internal static string QuickStrip_Resolution_Bigger_LimitTitle { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Bigger_LimitTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Increase Text Size. - /// - internal static string QuickStrip_Resolution_Bigger_Name { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Bigger_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Makes everything on screen smaller. - /// - internal static string QuickStrip_Resolution_Smaller_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Smaller_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decrease Text Size Everywhere. - /// - internal static string QuickStrip_Resolution_Smaller_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Smaller_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The text and icons are as small as they can be. - /// - internal static string QuickStrip_Resolution_Smaller_LimitMessage { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Smaller_LimitMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot Decrease Screen Zoom. - /// - internal static string QuickStrip_Resolution_Smaller_LimitTitle { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Smaller_LimitTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decrease Text Size. - /// - internal static string QuickStrip_Resolution_Smaller_Name { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Smaller_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Text Size. - /// - internal static string QuickStrip_Resolution_Title { - get { - return ResourceManager.GetString("QuickStrip_Resolution_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copy. - /// - internal static string QuickStrip_Snip_Button_Title { - get { - return ResourceManager.GetString("QuickStrip_Snip_Button_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Drag with your mouse to copy a part of screen. Paste into any document.. - /// - internal static string QuickStrip_Snip_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Snip_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Copies a selected part of the screen. - /// - internal static string QuickStrip_Snip_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Snip_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Snip a copy of part of screen. - /// - internal static string QuickStrip_Snip_Name { - get { - return ResourceManager.GetString("QuickStrip_Snip_Name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Snip. - /// - internal static string QuickStrip_Snip_Title { - get { - return ResourceManager.GetString("QuickStrip_Snip_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Make all the sounds quieter. - /// - internal static string QuickStrip_Volume_Down_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Down_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn the Volume Down. - /// - internal static string QuickStrip_Volume_Down_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Down_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The volume is all the way down. - /// - internal static string QuickStrip_Volume_Down_LimitMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Down_LimitMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot Turn the Volume Down. - /// - internal static string QuickStrip_Volume_Down_LimitTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Down_LimitTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn off all sounds from the computer. - /// - internal static string QuickStrip_Volume_Mute_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Mute_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute All Sounds. - /// - internal static string QuickStrip_Volume_Mute_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Mute_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmute by pressing the volume up or down button. - /// - internal static string QuickStrip_Volume_Mute_MutedMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Mute_MutedMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Sounds Are Muted. - /// - internal static string QuickStrip_Volume_Mute_MutedTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Mute_MutedTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mute. - /// - internal static string QuickStrip_Volume_Mute_Title { - get { - return ResourceManager.GetString("QuickStrip_Volume_Mute_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Volume. - /// - internal static string QuickStrip_Volume_Title { - get { - return ResourceManager.GetString("QuickStrip_Volume_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Return to the previous volume level. - /// - internal static string QuickStrip_Volume_Unmute_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Unmute_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unmute All Sounds. - /// - internal static string QuickStrip_Volume_Unmute_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Unmute_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Make all the sounds louder. - /// - internal static string QuickStrip_Volume_Up_HelpMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Up_HelpMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turn the Volume Up. - /// - internal static string QuickStrip_Volume_Up_HelpTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Up_HelpTitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The volume is all the way up. - /// - internal static string QuickStrip_Volume_Up_LimitMessage { - get { - return ResourceManager.GetString("QuickStrip_Volume_Up_LimitMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cannot Turn the Volume Up. - /// - internal static string QuickStrip_Volume_Up_LimitTitle { - get { - return ResourceManager.GetString("QuickStrip_Volume_Up_LimitTitle", resourceCulture); - } - } - } -} diff --git a/Morphic.Client/Properties/Resources.resx b/Morphic.Client/Properties/Resources.resx deleted file mode 100644 index f0cc5805..00000000 --- a/Morphic.Client/Properties/Resources.resx +++ /dev/null @@ -1,360 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Dialogs\LoginAnnounce.wav;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Find settings, sign in/out, customize buttons, move settings, and more. - - - Opens the Morphic Menu - - - - - - Turns Contrast off - - - Off - - - Can make the screen easier to read. To change settings, right-click on button. - - - Turns Contrast on - - - On - - - Contrast on, off - - - High Contrast - - - - - - Turns the Night Light feature off - - - Off - - - Reduces eye strain and makes it easier to fall asleep at night. - - - Turns the Night Light feature on - - - On - - - Night mode - - - Click Copy. Then click and drag mouse to take picture. Paste into any document. - - - Takes a picture of a selected part of the screen - - - Copy - - - Snip - - - Snip a copy of part of screen - - - Contrast & color - - - Contrast - - - Color - - - Color Vision - - - Turns Color Blindness filters on and off - - - Changes colors to adapt to color vision differences. To choose type, right-click on button. - - - Dark - - - Turns Dark Mode on and off - - - To change settings, right-click on button. - - - Dark mode - - - - Night - - - Night Light - - - - To change Magnifier settings, right-click on button. - - - Turns off Magnifier - - - Hide - - - Magnifier follows your mouse. Your mouse can click through the box. - - - Turns on a Magnifying lens - - - Show - - - Magnifier - - - Show Magnifier - - - Hide Magnifier - - - Select text to be read. Then click play button to read. To change settings, right-click on button. - - - Reads text that is selected - - - To change voice and speed settings, right-click on either button. - - - Stops reading - - - Stop Reading Select Text - - - Read Selected Text - - - Read Selected - - - Makes everything on the screen larger. - - - Increases Text Size everywhere - - - Increase Text Size - - - - - - Text Size cannot go bigger - - - Makes everything on the screen smaller. - - - Decreases Text Size everywhere - - - Decrease Text Size - - - - - - Text Size cannot go smaller - - - Text Size - - - Makes all sounds quieter. - - - Decreases the volume - - - - - - Volume cannot go quieter - - - Mutes your speakers - but does NOT mute your microphone. - - - Mutes all sounds from your computer - - - Turns Mute off - allowing sounds to come from your computer again. - - - Unmutes sounds - - - Mute - - - Volume - - - Turns Mute off - allowing sounds to come from your computer again. - - - Unmutes sounds - - - Makes all sounds louder. - - - Increases the volume - - - - - - Volume cannot go louder - - \ No newline at end of file diff --git a/Morphic.Client/Properties/launchSettings.json b/Morphic.Client/Properties/launchSettings.json deleted file mode 100644 index 5345bf16..00000000 --- a/Morphic.Client/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Morphic": { - "commandName": "Project", - "environmentVariables": { - "MORPHIC_DEBUG": "True", - "xMORPHIC_BAR": "../../../test-bar.json5" - } - } - } -} \ No newline at end of file diff --git a/Morphic.Client/Solutions/jaws2019.solutions.json b/Morphic.Client/Solutions/jaws2019.solutions.json deleted file mode 100644 index aab9de74..00000000 --- a/Morphic.Client/Solutions/jaws2019.solutions.json +++ /dev/null @@ -1,666 +0,0 @@ -[ - { - "id": "com.freedomscientific.jaws", - "settings": [ - { - "name": "podcast-notifications", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "FSCasts", - "key": "EnableNotifications" - } - } - ] - }, - { - "id": "com.freedomscientific.jaws.braille", - "settings": [ - { - "name": "cursor.all-dots", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "AllDotsBrailleCursor" - } - }, - { - "name": "cursor.auto-rotate", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleAutoRouteToCursor" - } - }, - { - "name": "cursor.move-active", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleMoveActiveCursor" - } - }, - { - "name": "cursor.blink-rate-ms", - "type": "integer", - "default": 500, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleCursorBlinkRate" - } - }, - { - "name": "attribute-rotation-delay-ms", - "type": "integer", - "default": 1000, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "AttributeRotationDelay" - } - }, - { - "name": "autopan.mode", - "type": "integer", - "default": 255, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "AutoPanMode" - } - }, - { - "name": "auto-detect-bluetooth", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleAutoDetectBluetooth" - } - }, - { - "name": "key-interrupt-speech", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleKeyInterruptSpeech" - } - }, - { - "name": "flash.enabled", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleMessages" - } - }, - { - "name": "flash.prefixes", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "MessagePrefixes" - } - }, - { - "name": "flash.verbosity", - "type": "integer", - "default": 0, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleVerbosity" - } - }, - { - "name": "flash.status", - "type": "string", - "default": "msg", - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "MessageStatusText" - } - }, - { - "name": "flash.timeout-ms", - "type": "integer", - "default": 5000, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "MessageTime" - } - }, - { - "name": "mode", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleMode" - } - }, - { - "name": "sleep", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "BrailleSleepMode" - } - }, - { - "name": "input.contracted", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "ContractedBrailleInput" - } - }, - { - "name": "eight-dot", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "EightDotBraille" - } - }, - { - "name": "filter-control-characters", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "FilterControlCharacters" - } - }, - { - "name": "generalize-bullets", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "GeneralizeBullets" - } - }, - { - "name": "grade2.supress-captial-signs", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "Grade2SuppressCapitalSigns" - } - }, - { - "name": "reverse-panning-buttons", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "ReversePanningButtons" - } - }, - { - "name": "reverse-structured-data", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "StructuredModeReverseOrder" - } - }, - { - "name": "status-cell-count", - "type": "integer", - "default": 4, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "UseHowManyStatusCells" - } - }, - { - "name": "rich-edits.use-screen-model", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "UseScreenModelForBrailleInRichEdits" - } - }, - { - "name": "word-wrap", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "Braille", - "key": "WordWrap" - } - } - ] - }, - { - "id": "com.freedomscientific.jaws.html", - "settings": [ - { - "name": "title.abbreviations", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "Abbreviations" - } - }, - { - "name": "title.acronyms", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "Acronyms" - } - }, - { - "name": "expand.abbreviations", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "ExpandAbbreviations" - } - }, - { - "name": "expand.acronyms", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "ExpandAcronyms" - } - }, - { - "name": "announce.access-keys", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "AccessKeys" - } - }, - { - "name": "announce.blockquote", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "BlockQuoteIndication" - } - }, - { - "name": "activex", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "EmbeddedActiveXSupport" - } - }, - { - "name": "filter.duplicate-links", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "FilterConsecutiveDuplicateLinks" - } - }, - { - "name": "filter.repeated-text", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "SkipPastRepeatedText" - } - }, - { - "name": "forms.field-prompt", - "type": "integer", - "default": 0, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "FormFieldPromptOptions" - } - }, - { - "name": "forms.auto-off", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "FormsModeAutoOff" - } - }, - { - "name": "graphics.mode", - "type": "integer", - "default": 0, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IncludeGraphics" - } - }, - { - "name": "graphics.link-last-resort", - "type": "integer", - "default": 0, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "GraphicalLinkLastResort" - } - }, - { - "name": "links.identify-type", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IdentifyLinkType" - } - }, - { - "name": "links.identify-same-page", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IdentifySamePageLinks" - } - }, - { - "name": "frames.ignore-inline", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IgnoreInlineFrames" - } - }, - { - "name": "tables.indicate-colspan", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IndicateColSpan" - } - }, - { - "name": "tables.mode", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TableDetection" - } - }, - { - "name": "tables.data.max-cell-text", - "type": "integer", - "default": 250, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblMaxCellTextLength" - } - }, - { - "name": "tables.data.min-cell-text", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblMinCellTextLength" - } - }, - { - "name": "tables.data.min-text-columns", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblMinTextColumns" - } - }, - { - "name": "tables.data.min-text-rows", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblMinTextRows" - } - }, - { - "name": "tables.data.min-valid-rows", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblMinValidDataRows" - } - }, - { - "name": "tables.data.min-valid-row-cells", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TblValidRowThreshold" - } - }, - { - "name": "indicate-attributes", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "IndicateElementAttributes" - } - }, - { - "name": "lists.indicate", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "ListIndication" - } - }, - { - "name": "max-line-length", - "type": "integer", - "default": 150, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "MaxLineLength" - } - }, - { - "name": "minimum-page-refresh-time", - "type": "integer", - "default": 0, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "PageRefreshFilter" - } - }, - { - "name": "read-on-load", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "SayAllOnDocumentLoad" - } - }, - { - "name": "smart-navigation", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "SmartNavigation" - } - }, - { - "name": "textblock.length", - "type": "integer", - "default": 25, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "TextBlockLength" - } - }, - { - "name": "use-legacy-ie", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "UseLegacyIESupport" - } - }, - { - "name": "wrap-key-navigation", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.ini", - "filename": "$(APPDATA)\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", - "section": "HTML", - "key": "WrapNavigation" - } - } - ] - } -] diff --git a/Morphic.Client/Solutions/jaws2020.solutions.json b/Morphic.Client/Solutions/jaws2020.solutions.json deleted file mode 100644 index 220357e3..00000000 --- a/Morphic.Client/Solutions/jaws2020.solutions.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "id": "com.freedomscientific.jaws2020", - "settings": [ - { - "name": "configuration", - "type": "files", - "default": null, - "handler": { - "type": "com.microsoft.windows.files", - "root": "$(APPDATA)\\Freedom Scientific\\JAWS\\2020\\Settings", - "files": [ - "enu\\*.JCF", - "VoiceProfiles\\*.VPF" - ] - } - } - ] - } -] \ No newline at end of file diff --git a/Morphic.Client/Solutions/windows.solutions.json b/Morphic.Client/Solutions/windows.solutions.json deleted file mode 100644 index bb2d8ba9..00000000 --- a/Morphic.Client/Solutions/windows.solutions.json +++ /dev/null @@ -1,503 +0,0 @@ -[ - { - "id": "com.microsoft.windows.display", - "settings": [ - { - "name": "zoom", - "type": "double", - "default": 1, - "handler": { - "type": "org.raisingthefloor.morphic.client", - "solution": "com.microsoft.windows.display", - "preference": "zoom" - } - }, - { - "name": "contrast.enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_CoreHighContrast_IsEnabled", - "value_type": "boolean" - } - }, - { - "name": "nightmode.enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Display_BlueLight_ManualToggleQuickAction", - "value_type": "boolean" - } - }, - { - "name": "lighttheme-apps.enabled", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Personalize_Color_AppsUseLightTheme", - "value_type": "boolean" - } - }, - { - "name": "lighttheme-windows.enabled", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Personalize_Color_SystemUsesLightTheme", - "value_type": "boolean" - } - }, - { - "name": "colorfilter.enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_ColorFiltering_IsEnabled", - "value_type": "boolean" - } - }, - { - "name": "colorfilter.shortcut.enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_ColorFiltering_IsShortcutKeyEnabled", - "value_type": "boolean" - } - }, - { - "name": "colorfilter.type", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_ColorFiltering_FilterType", - "value_type": "integer" - } - } - ] - }, - { - "id": "com.microsoft.windows.magnifier", - "settings": [ - { - "name": "enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Magnifier_IsEnabled", - "value_type": "boolean" - } - }, - { - "name": "mode", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "MagnificationMode", - "value_type": "dword" - } - }, - { - "name": "magnification", - "type": "integer", - "default": 200, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "Magnification", - "value_type": "dword" - } - }, - { - "name": "increment", - "type": "integer", - "default": 100, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "ZoomIncrement", - "value_type": "dword" - } - }, - { - "name": "smoothing", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "UseBitmapSmoothing", - "value_type": "dword" - } - }, - { - "name": "lens.height", - "type": "integer", - "default": 10, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "LensHeight", - "value_type": "dword" - } - }, - { - "name": "lens.width", - "type": "integer", - "default": 10, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "LensWidth", - "value_type": "dword" - } - }, - { - "name": "follow.mouse", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "FollowMouse", - "value_type": "dword" - } - }, - { - "name": "follow.focus", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "FollowFocus", - "value_type": "dword" - } - }, - { - "name": "follow.caret", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "FollowCaret", - "value_type": "dword" - } - }, - { - "name": "follow.narrator", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "FollowNarrator", - "value_type": "dword" - } - }, - { - "name": "invert", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "Invert", - "value_type": "dword" - } - }, - { - "name": "fade", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.registry", - "key_name": "HKEY_CURRENT_USER\\Software\\Microsoft\\ScreenMagnifier", - "value_name": "FadeToMagIcon", - "value_type": "dword" - } - } - ] - }, - { - "id": "com.microsoft.windows.narrator", - "settings": [ - { - "name": "enabled", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsEnabled", - "value_type": "boolean" - } - }, - { - "name": "follow.keyboard", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsFollowKeyboardFocusEnabled", - "value_type": "boolean" - } - }, - { - "name": "follow.pointer", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsFollowMouseEnabled", - "value_type": "boolean" - } - }, - { - "name": "follow.insertion", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsFollowInsertionEnabled", - "value_type": "boolean" - } - }, - { - "name": "echo.characters", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsEchoCharacterEnabled", - "value_type": "boolean" - } - }, - { - "name": "echo.words", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsEchoWordEnabled", - "value_type": "boolean" - } - }, - { - "name": "voice-errors", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_AreNarratorErrorsVoiced", - "value_type": "boolean" - } - }, - { - "name": "fastkey", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsFastKeyEntryEnabled", - "value_type": "boolean" - } - }, - { - "name": "intonation-pause", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsIntonationPauseEnabled", - "value_type": "boolean" - } - }, - { - "name": "lock-key", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsNarratorKeyLocked", - "value_type": "boolean" - } - }, - { - "name": "highlight-cursor", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsHighlightCursorEnabled", - "value_type": "boolean" - } - }, - { - "name": "audio-cues", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsPlayAudioCuesEnabled", - "value_type": "boolean" - } - }, - { - "name": "read.intent", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsReadingWithIntentEnabled", - "value_type": "boolean" - } - }, - { - "name": "read.hints", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsReadHintsEnabled", - "value_type": "boolean" - } - }, - { - "name": "read.pointer", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsReadMouseEnabled", - "value_type": "boolean" - } - }, - { - "name": "speech.speed", - "type": "integer", - "default": 10, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_SpeechSpeed", - "value_type": "integer" - } - }, - { - "name": "speech.pitch", - "type": "integer", - "default": 10, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_SpeechPitch", - "value_type": "integer" - } - }, - { - "name": "speech.volume", - "type": "integer", - "default": 100, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_SpeechVolume", - "value_type": "integer" - } - }, - { - "name": "context.amount", - "type": "integer", - "default": 2, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_ContextualReadingAmount", - "value_type": "idPrefixedEnum" - } - }, - { - "name": "context.order", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_ContextualReadingOrder", - "value_type": "string", - "integer_map": [ - "SystemSettings_Accessibility_Narrator_ContextualReadingOrderBefore", - "SystemSettings_Accessibility_Narrator_ContextualReadingOrderAfter" - ] - } - }, - { - "name": "duck", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsDuckAudioEnabled", - "value_type": "boolean" - } - }, - { - "name": "shortcut.enabled", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsShortcutKeyEnabled", - "value_type": "boolean" - } - }, - { - "name": "verbosity", - "type": "integer", - "default": 1, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_UserVerbosityAmount", - "value_type": "idPrefixedEnum" - } - }, - { - "name": "deatailed-feedback", - "type": "boolean", - "default": true, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsAlwaysLoggingEnabled", - "value_type": "boolean" - } - }, - { - "name": "autostart.after-signin", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled", - "value_type": "boolean" - } - }, - { - "name": "autostart.before-signin", - "type": "boolean", - "default": false, - "handler": { - "type": "com.microsoft.windows.system", - "setting_id": "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled", - "value_type": "boolean" - } - } - ] - } -] diff --git a/Morphic.Client/StringDictionarySetting.cs b/Morphic.Client/StringDictionarySetting.cs deleted file mode 100644 index 018a13de..00000000 --- a/Morphic.Client/StringDictionarySetting.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections; -using System.Collections.Specialized; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Morphic.Client -{ - public class StringDictionarySetting : StringDictionary, IXmlSerializable - { - public XmlSchema GetSchema() - { - return null!; - } - - public void ReadXml(XmlReader reader) - { - var endName = GetType().Name; - while (reader.NodeType != XmlNodeType.EndElement || reader.LocalName != endName) - { - reader.Read(); - if (reader.LocalName == "Entry") - { - Add(reader["Key"], reader["Value"]); - } - } - } - - public void WriteXml(XmlWriter writer) - { - foreach (var pair in this) - { - if (pair is DictionaryEntry entry) - { - writer.WriteStartElement("Entry"); - writer.WriteAttributeString("Key", entry.Key as string); - writer.WriteAttributeString("Value", entry.Value as string); - writer.WriteEndElement(); - } - } - } - } -} diff --git a/Morphic.Client/TextToSpeechHelper.cs b/Morphic.Client/TextToSpeechHelper.cs deleted file mode 100644 index cce3758f..00000000 --- a/Morphic.Client/TextToSpeechHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Morphic.Core; -using System; -using System.IO; -using System.Media; -using System.Threading.Tasks; -using Windows.Media.SpeechSynthesis; - -namespace Morphic.Client -{ - public class TextToSpeechHelper : IDisposable - { - private static readonly Lazy _lazyTextToSpeechHelper = new Lazy(() => new TextToSpeechHelper()); - public static TextToSpeechHelper Instance => _lazyTextToSpeechHelper.Value; - - private readonly SoundPlayer _speechPlayer; - - private TextToSpeechHelper() - { - _speechPlayer = new SoundPlayer(); - } - - public void Stop() - { - _speechPlayer.Stop(); - } - - public async Task> Say(string text) - { - Stop(); - - using var synth = new SpeechSynthesizer(); - using var stream = await synth.SynthesizeTextToStreamAsync(text); - - // NOTE: we cannot use the speech synthesizer directly using SpeakAsync/SpeakAsyncCancelAll because Windows.Speech.SpeechSynthesis and these corresponding methods are not supported in .NET 5 (i.e. only in .NET Framework) - _speechPlayer.Stream = stream.AsStream(); - - // NOTE: Play() loads and plays the sound in a new thread asynchronously; we could await on LoadAsync and then call Play, but that could create a contention if - // Stop() was called before the LoadAsync callback completed and therefore before Play() got called - _speechPlayer.Play(); - - return MorphicResult.OkResult(); - } - - public void Dispose() - { - Stop(); - - _speechPlayer.Dispose(); - } - } -} diff --git a/Morphic.Client/UserSettings.Designer.cs b/Morphic.Client/UserSettings.Designer.cs deleted file mode 100644 index 3272553d..00000000 --- a/Morphic.Client/UserSettings.Designer.cs +++ /dev/null @@ -1,49 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Morphic.Client { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.5.0.0")] - internal sealed partial class UserSettings : global::System.Configuration.ApplicationSettingsBase { - - private static UserSettings defaultInstance = ((UserSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new UserSettings()))); - - public static UserSettings Default { - get { - return defaultInstance; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string UserId { - get { - return ((string)(this["UserId"])); - } - set { - this["UserId"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - public global::Morphic.Client.StringDictionarySetting UsernamesById { - get { - return ((global::Morphic.Client.StringDictionarySetting)(this["UsernamesById"])); - } - set { - this["UsernamesById"] = value; - } - } - } -} diff --git a/Morphic.Client/UserSettings.settings b/Morphic.Client/UserSettings.settings deleted file mode 100644 index 77203a17..00000000 --- a/Morphic.Client/UserSettings.settings +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/WinApi.cs b/Morphic.Client/WinApi.cs deleted file mode 100644 index 26522fce..00000000 --- a/Morphic.Client/WinApi.cs +++ /dev/null @@ -1,1005 +0,0 @@ -// WinApi.cs: Utilities for using the Windows API. -// -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt - -namespace Morphic.Client -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Runtime.InteropServices; - using System.Windows; - - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Names come from the Windows API")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Names come from the Windows API")] - internal static class WinApi - { - #region Win32 error codes - public const uint ERROR_SUCCESS = 0; - #endregion - - #region Window Positioning - - public static RECT ToRECT(this Rect rc) - { - return new RECT(rc); - } - - public static POINT ToPOINT(this Point pt) - { - return new POINT(pt); - } - - [StructLayout(LayoutKind.Sequential)] - public struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - - /// - /// Creates the struct using the data from the pointer. - /// - /// The pointer to the unmanaged memory. - /// A RECT structure, from the data pointed to by the pointer. - public static RECT FromPointer(IntPtr pointer) - { - return Marshal.PtrToStructure(pointer)!; - } - - /// - /// Copies the values from this struct to the pointer. - /// - /// The pointer to the unmanaged memory. - public void CopyToPointer(IntPtr pointer) - { - Marshal.StructureToPtr(this, pointer, false); - } - - /// - /// Creates a .NET Rect from this win32 RECT. - /// - /// - public Rect ToRect() - { - return new Rect(this.Left, this.Top, this.Right - this.Left, this.Bottom - this.Top); - } - - public PInvoke.RECT ToPInvokeRect() - { - return new PInvoke.RECT() { left = this.Left, top = this.Top, right = this.Right, bottom = this.Bottom }; - } - - /// - /// Creates a win32 RECT from a .NET Rect. - /// - /// The rectangle. - public RECT(Rect rect) - { - this.Left = (int)rect.Left; - this.Top = (int)rect.Top; - this.Right = (int)rect.Right; - this.Bottom = (int)rect.Bottom; - } - - public static RECT Empty - { - get - { - return new RECT(new Rect(0, 0, 0, 0)); - } - } - - public override string ToString() - { - return $"{this.Left} {this.Top} {this.Right} {this.Bottom}"; - } - - public bool HasNonZeroWidthOrHeight() - { - return ((this.Left == this.Right) || (this.Top == this.Bottom)); - } - - public bool IsInside(RECT rect) - { - return ((this.Left >= rect.Left) && (this.Right <= rect.Right) && (this.Top >= rect.Top) && (this.Bottom <= rect.Bottom)); - } - - public bool Intersects(RECT rect) - { - bool overlapsHorizontally = false; - bool overlapsVertically = false; - - // horizontal check - if ((this.Right > rect.Left) && (this.Left < rect.Right)) - { - // partially or fully overlaps horizontally - overlapsHorizontally = true; - } - - // vertical check - if ((this.Bottom > rect.Top) && (this.Top < rect.Bottom)) - { - // partially or fully overlaps vertically - overlapsVertically = true; - } - - if ((overlapsHorizontally == true) && (overlapsVertically == true)) { - return true; - } - - // if we could not find overlap, then return false - return false; - } - - public static bool operator ==(RECT lhs, RECT rhs) - { - if ((lhs.Left == rhs.Left) && - (lhs.Top == rhs.Top) && - (lhs.Right == rhs.Right) && - (lhs.Bottom == rhs.Bottom)) - { - return true; - } - else - { - return false; - } - } - - public static bool operator !=(RECT lhs, RECT rhs) - { - if ((lhs.Left != rhs.Left) || - (lhs.Top != rhs.Top) || - (lhs.Right != rhs.Right) || - (lhs.Bottom != rhs.Bottom)) - { - return true; - } - else - { - return false; - } - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct WINDOWPOS - { - public IntPtr hwndInsertAfter; - public IntPtr hwnd; - public int x; - public int y; - public int cx; - public int cy; - public int flags; - - /// - /// Creates the struct using the data from the pointer. - /// - /// The pointer to the unmanaged memory. - /// A RECT structure, from the data pointed to by the pointer. - public static WINDOWPOS FromPointer(IntPtr pointer) - { - return Marshal.PtrToStructure(pointer)!; - } - - /// - /// Copies the values from this struct to the pointer. - /// - /// The pointer to the unmanaged memory. - public void CopyToPointer(IntPtr pointer) - { - Marshal.StructureToPtr(this, pointer, false); - } - } - - public const int SWP_NOSIZE = 0x1; - public const int SWP_NOMOVE = 0x2; - - public const int WM_WINDOWPOSCHANGING = 0x0046; - public const int WM_WINDOWPOSCHANGED = 0x0047; - public const int WM_SIZING = 0x0214; - public const int WM_MOVING = 0x0216; - public const int WM_ENTERSIZEMOVE = 0x0231; - public const int WM_EXITSIZEMOVE = 0x0232; - public const int WM_SYSCOMMAND = 0x0112; - - public const int SC_DRAGMOVE = 0xf012; - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool GetCursorPos(ref POINT pt); - - [StructLayout(LayoutKind.Sequential)] - public struct POINT - { - public int X; - public int Y; - - public POINT(int x, int y) - { - this.X = x; - this.Y = y; - } - - public POINT(Point pt) : this((int)pt.X, (int)pt.Y) - { - } - - public Point ToPoint() - { - return new Point(this.X, this.Y); - } - } - - public static Point GetCursorPos() - { - POINT pt = new POINT(); - WinApi.GetCursorPos(ref pt); - return pt.ToPoint(); - } - - public static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong) - { - return IntPtr.Size == 8 - ? SetWindowLongPtr64(hWnd, nIndex, dwNewLong) - : SetWindowLong32(hWnd, nIndex, dwNewLong); - } - - public static IntPtr GetWindowLong(IntPtr hWnd, int nIndex) - { - return IntPtr.Size == 8 - ? GetWindowLongPtr64(hWnd, nIndex) - : GetWindowLong32(hWnd, nIndex); - } - - /// - /// Better than Window.DragMove - this doesn't fire the click event of the control on which the mouse - /// is down, and doesn't require a mouse button to be down. - /// - /// - public static void DragMove(IntPtr hWnd) - { - SendMessage(hWnd, WinApi.WM_SYSCOMMAND, WinApi.SC_DRAGMOVE, IntPtr.Zero); - } - - public const int GWL_STYLE = -16; - public const int GWL_EXSTYLE = -20; - public const int WS_SIZEBOX = 0x00040000; - public const int WS_EX_TOOLWINDOW = 0x00000080; - public const int SPI_GETWORKAREA = 0x0030; - - - [DllImport("user32.dll", EntryPoint = "SetWindowLong")] - private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] - private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - [DllImport("user32.dll", EntryPoint = "GetWindowLong")] - private static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] - private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll")] - public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); - - [DllImport("user32.dll", EntryPoint = "SystemParametersInfoW")] - internal static extern bool SystemParametersInfoRect(int uiAction, int uiParam, ref RECT pvParam, int fWinIni); - - [DllImport("user32.dll")] - internal static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); - - [DllImport("user32.dll", SetLastError = true)] - internal static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); - - #endregion - - #region Monitor info - - [StructLayout(LayoutKind.Sequential)] - public struct MONITORINFO - { - public int cbSize; - public RECT rcMonitor; - public RECT rcWork; - public uint dwFlags; - - public MONITORINFO(IntPtr hMonitor) : this() - { - this.cbSize = Marshal.SizeOf(this); - // NOTE: we are not yet checking for a failure result from this API - GetMonitorInfo(hMonitor, ref this); - } - } - - internal enum MonitorDefault - { - MONITOR_DEFAULTTONULL, - MONITOR_DEFAULTTOPRIMARY, - MONITOR_DEFAULTTONEAREST - } - - public static MONITORINFO GetMonitorInfo() => GetMonitorInfo(IntPtr.Zero); - - public static MONITORINFO GetMonitorInfo(IntPtr hwnd) - { - IntPtr monitor = MonitorFromWindow(hwnd, MonitorDefault.MONITOR_DEFAULTTOPRIMARY); - return new MONITORINFO(monitor); - } - - public static MONITORINFO GetMonitorInfo(Point pt) - { - IntPtr monitor = MonitorFromPoint(pt.ToPOINT(), MonitorDefault.MONITOR_DEFAULTTONEAREST); - return new MONITORINFO(monitor); - } - - public static IEnumerable GetMonitorInfoAll() - { - List monitors = new List(); - - RECT screen = GetVirtualScreen().ToRECT(); - EnumDisplayMonitors(IntPtr.Zero, ref screen, - (IntPtr monitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr data) => - { - monitors.Add(GetMonitorInfo(monitor)); - return true; - }, IntPtr.Zero); - - return monitors.ToArray(); - } - - public static Rect GetVirtualScreen() - { - return new Rect( - GetSystemMetrics(SM_XVIRTUALSCREEN), - GetSystemMetrics(SM_YVIRTUALSCREEN), - GetSystemMetrics(SM_CXVIRTUALSCREEN), - GetSystemMetrics(SM_CYVIRTUALSCREEN)); - } - - public const int SM_XVIRTUALSCREEN = 76; - public const int SM_YVIRTUALSCREEN = 77; - public const int SM_CXVIRTUALSCREEN = 78; - public const int SM_CYVIRTUALSCREEN = 79; - public const int SM_CMONITORS = 80; - - [DllImport("User32.dll")] - private static extern IntPtr MonitorFromPoint(POINT pt, MonitorDefault dwFlags); - - [DllImport("User32.dll")] - private static extern IntPtr MonitorFromWindow(IntPtr hWnd, MonitorDefault dwFlags); - - [DllImport("User32.dll")] - private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); - - [DllImport("user32.dll")] - internal static extern int GetSystemMetrics(int smIndex); - - [DllImport("user32.dll")] - private static extern bool EnumDisplayMonitors(IntPtr hdc, ref RECT lprcClip, EnumMonitorsDelegate lpfnEnum, IntPtr dwData); - - private delegate bool EnumMonitorsDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); - - #endregion - - #region App Bar API - - [StructLayout(LayoutKind.Sequential)] - internal struct APPBARDATA - { - public int cbSize; - public IntPtr hWnd; - public uint uCallbackMessage; - public uint uEdge; - public RECT rc; - public IntPtr lParam; - } - - [Flags] - internal enum DWMWINDOWATTRIBUTE - { - DWMA_NCRENDERING_ENABLED = 1, - DWMA_NCRENDERING_POLICY, - DWMA_TRANSITIONS_FORCEDISABLED, - DWMA_ALLOW_NCPAINT, - DWMA_CPATION_BUTTON_BOUNDS, - DWMA_NONCLIENT_RTL_LAYOUT, - DWMA_FORCE_ICONIC_REPRESENTATION, - DWMA_FLIP3D_POLICY, - DWMA_EXTENDED_FRAME_BOUNDS, - DWMA_HAS_ICONIC_BITMAP, - DWMA_DISALLOW_PEEK, - DWMA_EXCLUDED_FROM_PEEK, - DWMA_LAST - } - - [Flags] - internal enum DWMNCRenderingPolicy - { - UseWindowStyle, - Disabled, - Enabled, - Last - } - - internal enum ABMessage - { - ABM_NEW = 0, - ABM_REMOVE, - ABM_QUERYPOS, - ABM_SETPOS, - ABM_GETSTATE, - ABM_GETTASKBARPOS, - ABM_ACTIVATE, - ABM_GETAUTOHIDEBAR, - ABM_SETAUTOHIDEBAR, - ABM_WINDOWPOSCHANGED, - ABM_SETSTATE - } - - internal enum ABNotify - { - ABN_STATECHANGE = 0, - ABN_POSCHANGED, - ABN_FULLSCREENAPP, - ABN_WINDOWARRANGE - } - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - internal static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); - - [DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)] - internal static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData); - - [DllImport("User32.dll", CharSet = CharSet.Auto)] - internal static extern uint RegisterWindowMessage(string lpString); - - [DllImport("dwmapi.dll")] - internal static extern int DwmSetWindowAttribute(IntPtr hWnd, int attr, ref int attrValue, int attrSize); - -// internal static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); - internal static readonly IntPtr HWND_MESSAGE = new IntPtr(-3); - - #endregion - - #region Window Creation and Management - - public static bool ActivateWindow(IntPtr hwnd) - { - if (IsIconic(hwnd)) - { - ShowWindow(hwnd, 9); - } - - return SetForegroundWindow(hwnd); - } - - [DllImport("gdi32.dll")] - internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); - - [DllImport("gdi32.dll")] - internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO pbmi, uint usage, out IntPtr ppvBits, IntPtr hSection, uint offset); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern IntPtr CreateWindowEx( - WindowStylesEx dwExStyle, - IntPtr lpClassName, - string? lpWindowName, - WindowStyles dwStyle, - int x, - int y, - int nWidth, - int nHeight, - IntPtr hWndParent, - IntPtr hMenu, - IntPtr hInstance, - IntPtr lpParam); - - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern IntPtr CreateWindowEx( - WindowStylesEx dwExStyle, - string lpClassName, - string? lpWindowName, - WindowStyles dwStyle, - int x, - int y, - int nWidth, - int nHeight, - IntPtr hWndParent, - IntPtr hMenu, - IntPtr hInstance, - IntPtr lpParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - [DllImport("gdi32.dll")] - internal static extern bool DeleteDC(IntPtr hdc); - - [DllImport("gdi32.dll")] - internal static extern bool DeleteObject(IntPtr ho); - - [DllImport("user32.dll")] - internal static extern bool DestroyWindow(IntPtr hWnd); - - internal delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string? lpszWindow); - - [DllImport("user32.dll")] - internal static extern UInt32 GetDpiForWindow(IntPtr hwnd); - - [DllImport("user32.dll")] - internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); - - [DllImport("user32.dll")] - private static extern bool IsIconic(IntPtr hWnd); - - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - internal static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); - // - internal enum Cursors - { - IDC_APPSTARTING = 32650, - IDC_ARROW = 32512, - IDC_CROSS = 32515, - IDC_HAND = 32649, - IDC_HELP = 32651, - IDC_IBEAM = 32513, - IDC_ICON = 32641, - IDC_NO = 32648, - IDC_SIZE = 32640, - IDC_SIZEALL = 32646, - IDC_SIZENESW = 32643, - IDC_SIZENS = 32645, - IDC_SIZENWSE = 32642, - IDC_SIZEWE = 32644, - IDC_UPARROW = 32516, - IDC_WAIT = 32514, - } - - // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints - // NOTE: this signature is the POINT option (in which cPoints must always be set to 1). - [DllImport("user32.dll", SetLastError = true)] - internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In] ref POINT lpPoints, uint cPoints); - - // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints - // NOTE: this signature is the RECT option (in which cPoints must always be set to 2). - [DllImport("user32.dll", SetLastError = true)] - internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In] ref RECT lpPoints, uint cPoints); - - [DllImport("user32.dll")] - internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags); - - // source for values: http://www.pinvoke.net/default.aspx/Enums/RedrawWindowFlags.html - internal enum RedrawWindowFlags : uint - { - // invalidation flags - RDW_ERASE = 0x4, - RDW_FRAME = 0x400, - RDW_INTERNALPAINT = 0x2, - RDW_INVALIDATE = 0x1, - // validation flags - RDW_NOERASE = 0x20, - RDW_NOFRAME = 0x800, - RDW_NOINTERNALPAINT = 0x10, - RDW_VALIDATE = 0x8, - // repainting flags - RDW_ERASENOW = 0x200, - RDW_UPDATENOW = 0x100, - // misc. control flags - RDW_ALLCHILDREN = 0x80, - RDW_NOCHILDREN = 0x40 - } - - [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - internal static extern ushort RegisterClassEx([In] ref WNDCLASSEX lpWndClass); - - [DllImport("gdi32.dll")] - internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); - - [DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport("user32.dll")] - internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags); - - internal enum SetWindowPosFlags : uint - { - SWP_ASYNCWINDOWPOS = 0x4000, - SWP_DEFERERASE = 0x2000, - SWP_DRAWFRAME = 0x0020, - SWP_FRAMECHANGED = 0x0020, - SWP_HIDEWINDOW = 0x0080, - SWP_NOACTIVATE = 0x0010, - SWP_NOCOPYBITS = 0x0100, - SWP_NOMOVE = 0x0002, - SWP_NOOWNERZORDER = 0x0200, - SWP_NOREDRAW = 0x0008, - SWP_NOREPOSITION = 0x0200, - SWP_NOSENDCHANGING = 0x0400, - SWP_NOSIZE = 0x0001, - SWP_NOZORDER = 0x0004, - SWP_SHOWWINDOW = 0x0040 - } - - internal struct TOOLINFO - { - public uint cbSize; - public uint uFlags; - public IntPtr hwnd; - public IntPtr uId; - public RECT rect; - public IntPtr hinst; - [MarshalAs(UnmanagedType.LPTStr)] - public string? lpszText; - public IntPtr lParam; - //public IntPtr reserved; // NOTE: this exists in the official documentation but adding it causes SendMessage to fail; pinvoke.net leaves it out and so do we - } - - [DllImport("user32.dll")] - internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); - - [StructLayout(LayoutKind.Sequential)] - internal struct TRACKMOUSEEVENT - { - public uint cbSize; - public TMEFlags dwFlags; - public IntPtr hWnd; - public uint dwHoverTime; - - public TRACKMOUSEEVENT(TMEFlags dwFlags, IntPtr hWnd, uint dwHoverTime) - { - this.cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)); - this.dwFlags = dwFlags; - this.hWnd = hWnd; - this.dwHoverTime = dwHoverTime; - } - } - - // WinUser.h (Windows 10 1809 SDK) - internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; - - [Flags] - internal enum TMEFlags : uint - { - TME_CANCEL = 0x80000000, - TME_HOVER = 0x00000001, - TME_LEAVE = 0x00000002, - TME_NONCLIENT = 0x00000010, - TME_QUERY = 0x40000000 - } - - [DllImport("user32.dll")] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [StructLayout(LayoutKind.Sequential)] - internal struct BITMAPINFO - { - public BITMAPINFOHEADER bmiHeader; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // NOTE: in other implementations, this was represented as a uint instead (with 256 elements instead of 1 element) - public RGBQUAD[] bmiColors; - } - - [StructLayout(LayoutKind.Sequential)] - public struct BITMAPINFOHEADER - { - public uint biSize; - public int biWidth; - public int biHeight; - public ushort biPlanes; - public ushort biBitCount; - public BitmapCompressionType biCompression; - public uint biSizeImage; - public int biXPelsPerMeter; - public int biYPelsPerMeter; - public uint biClrUsed; - public uint biClrImportant; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct RGBQUAD - { - byte rgbBlue; - byte rgbGreen; - byte rgbRed; - byte rgbReserved; - } - - // wingdi.h (Windows 10 1809 SDK) - internal enum BitmapCompressionType : uint - { - BI_RGB = 0, - BI_RLE8 = 1, - BI_RLE4 = 2, - BI_BITFIELDS = 3, - BI_JPEG = 4, - BI_PNG = 5 - } - - internal static readonly IntPtr HWND_TOP = new IntPtr(0); - internal static readonly IntPtr HWND_BOTTOM = new IntPtr(1); - internal static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); - internal static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); - - internal const int MA_NOACTIVATEANDEAT = 4; - - internal const int CW_USEDEFAULT = unchecked((int)0x80000000); - - internal const string TOOLTIPS_CLASS = "tooltips_class32"; - - internal const uint WM_USER = 0x0400; - - internal const uint TTM_ADDTOOL = WM_USER + 50; - internal const uint TTM_DELTOOL = WM_USER + 51; - - // - - internal const uint MK_LBUTTON = 0x0001; - internal const uint MK_RBUTTON = 0x0002; - - internal const uint S_OK = 0; - - // WinUser.h (Windows 10 1809 SDK) - public enum WindowMessage : uint - { - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-create - WM_CREATE = 0x0001, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy - WM_DESTROY = 0x0002, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-displaychange - WM_DISPLAYCHANGE = 0x007E, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-erasebkgnd - WM_ERASEBKGND = 0x0014, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown - WM_LBUTTONDOWN = 0x0201, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup - WM_LBUTTONUP = 0x0202, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate - WM_MOUSEACTIVATE = 0x0021, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseleave - WM_MOUSELEAVE = 0x02A3, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove - WM_MOUSEMOVE = 0x0200, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest - WM_NCHITTEST = 0x0084, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-ncpaint - WM_NCPAINT = 0x0085, - - // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-paint - WM_PAINT = 0x000F, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged - WM_WINDOWPOSCHANGED = 0x0047, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanging - WM_WINDOWPOSCHANGING = 0x0046, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondown - WM_RBUTTONDOWN = 0x0204, - - // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttonup - WM_RBUTTONUP = 0x0205, - - // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor - WM_SETCURSOR = 0x0020, - - // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size - WM_SIZE = 0x0005, - } - - [Flags] - internal enum WindowStyles : uint - { - WS_BORDER = 0x00800000, - WS_CAPTION = 0x00C00000, - WS_CHILD = 0x40000000, - WS_CHILDWINDOW = 0x40000000, - WS_CLIPCHILDREN = 0x02000000, - WS_CLIPSIBLINGS = 0x04000000, - WS_DISABLED = 0x08000000, - WS_DLGFRAME = 0x00400000, - WS_GROUP = 0x00020000, - WS_HSCROLL = 0x00100000, - WS_ICONIC = 0x20000000, - WS_MAXIMIZE = 0x01000000, - WS_MAXIMIZEBOX = 0x00010000, - WS_MINIMIZE = 0x20000000, - WS_MINIMIZEBOX = 0x00020000, - WS_OVERLAPPED = 0x00000000, - WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, - WS_POPUP = 0x80000000, - WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, - WS_SIZEBOX = 0x00040000, - WS_SYSMENU = 0x00080000, - WS_TABSTOP = 0x00010000, - WS_THICKFRAME = 0x00040000, - WS_TILED = 0x00000000, - WS_TILEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, - WS_VISIBLE = 0x10000000, - WS_VSCROLL = 0x00200000 - } - - [Flags] - internal enum WindowStylesEx : uint - { - WS_EX_ACCEPTFILES = 0x00000010, - WS_EX_APPWINDOW = 0x00040000, - WS_EX_CLIENTEDGE = 0x00000200, - WS_EX_COMPOSITED = 0x02000000, - WS_EX_CONTEXTHELP = 0x00000400, - WS_EX_CONTROLPARENT = 0x00010000, - WS_EX_DLGMODALFRAME = 0x00000001, - WS_EX_LAYERED = 0x00080000, - WS_EX_LAYOUTRTL = 0x00400000, - WS_EX_LEFT = 0x00000000, - WS_EX_LEFTSCROLLBAR = 0x00004000, - WS_EX_LTRREADING = 0x00000000, - WS_EX_MDICHILD = 0x00000040, - WS_EX_NOACTIVATE = 0x08000000, - WS_EX_NOINHERITLAYOUT = 0x00100000, - WS_EX_NOPARENTNOTIFY = 0x00000004, - WS_EX_NOREDIRECTIONBITMAP = 0x00200000, - WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, - WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, - WS_EX_RIGHT = 0x00001000, - WS_EX_RIGHTSCROLLBAR = 0x00000000, - WS_EX_RTLREADING = 0x00002000, - WS_EX_STATICEDGE = 0x00020000, - WS_EX_TOOLWINDOW = 0x00000080, - WS_EX_TOPMOST = 0x00000008, - WS_EX_TRANSPARENT = 0x00000020, - WS_EX_WINDOWEDGE = 0x00000100 - } - - internal const uint TTF_SUBCLASS = 0x0010; - - internal const uint TTS_ALWAYSTIP = 0x01; - internal const uint TTS_NOPREFIX = 0x02; - internal const uint TTS_BALLOON = 0x40; - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - internal struct WNDCLASSEX - { - [MarshalAs(UnmanagedType.U4)] - public uint cbSize; - [MarshalAs(UnmanagedType.U4)] - //public ClassStyles style; - public uint style; - public IntPtr lpfnWndProc; - public int cbClsExtra; - public int cbWndExtra; - public IntPtr hInstance; - public IntPtr hIcon; - public IntPtr hCursor; - public IntPtr hbrBackground; - public string lpszMenuName; - public string lpszClassName; - public IntPtr hIconSm; - } - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - internal delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); - - #endregion - - #region Window Painting - - // NOTE: per pinvoke.net, this function is called "GdiAlphaBlend" even though the Microsoft documentation calls it AlphaBlend - [DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")] - internal static extern bool AlphaBlend(IntPtr hdcDest, int xOriginDest, int yOriginDest, int wDest, int hDest, IntPtr hdcSrc, int xOriginSrc, int yOriginSrc, int wSrc, int hSrc, BLENDFUNCTION ftn); - - [StructLayout(LayoutKind.Sequential)] - public struct BLENDFUNCTION - { - public byte BlendOp; - public byte BlendFlags; - public byte SourceConstantAlpha; - public byte AlphaFormat; - } - - internal const byte AC_SRC_OVER = 0x00; - //internal const byte AC_SRC_ALPHA = 0x01; - - internal const uint DIB_RGB_COLORS = 0; - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-beginbufferedpaint - [DllImport("uxtheme.dll")] - internal static extern IntPtr BeginBufferedPaint(IntPtr hdcTarget, [In] ref RECT prcTarget, BP_BUFFERFORMAT dwFormat, IntPtr pPaintParams, out IntPtr phdc); - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/ne-uxtheme-bp_bufferformat - internal enum BP_BUFFERFORMAT: uint - { - BPBF_COMPATIBLEBITMAP, - BPBF_DIB, - BPBF_TOPDOWNDIB, - BPBF_TOPDOWNMONODIB - } - - [DllImport("user32.dll")] - internal static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintclear - [DllImport("uxtheme.dll")] - internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, ref RECT prc); - // - [DllImport("uxtheme.dll")] - internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, IntPtr prc); - - [DllImport("uxtheme.dll")] - internal static extern int BufferedPaintInit(); - - [DllImport("uxtheme.dll")] - internal static extern int BufferedPaintUnInit(); - - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawiconex - [DllImport("user32.dll")] - internal static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, DrawIconFlags diFlags); - - [Flags] - internal enum DrawIconFlags: uint - { - DI_COMPAT = 0x0004, - DI_DEFAULTSIZE = 0x0008, - DI_IMAGE = 0x0002, - DI_MASK = 0x0001, - DI_NOMIRROR = 0x0010, - DI_NORMAL = DI_IMAGE | DI_MASK // 0x0003 - } - - // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-endbufferedpaint - [DllImport("uxtheme.dll")] - internal static extern uint EndBufferedPaint(IntPtr hBufferedPaint, bool fUpdateTarget); - - [DllImport("user32.dll")] - internal static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint); - - // TODO: remove FILLRECT! - [DllImport("user32.dll")] - internal static extern int FillRect(IntPtr hDC, [In] ref RECT lprc, IntPtr hbr); - - // - - [StructLayout(LayoutKind.Sequential)] - internal struct PAINTSTRUCT - { - public IntPtr hdc; - public bool fErase; - public RECT rcPaint; - public bool fRestore; - public bool fIncUpdate; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - public byte[] rgbReserved; - } - - #endregion - } -} \ No newline at end of file diff --git a/Morphic.Client/WindowsUserSettings.cs b/Morphic.Client/WindowsUserSettings.cs deleted file mode 100644 index 651f96cd..00000000 --- a/Morphic.Client/WindowsUserSettings.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using Morphic.Core; - -namespace Morphic.Client -{ - public class WindowsUserSettings : IUserSettings - { - public string? UserId - { - get => UserSettings.Default.UserId; - set - { - UserSettings.Default.UserId = value; - UserSettings.Default.Save(); - } - } - - public string? GetUsernameForId(string userId) - { - if (UserSettings.Default.UsernamesById is null) - { - return null; - } - return UserSettings.Default.UsernamesById[UserId]; - } - - public void SetUsernameForId(string username, string userId) - { - if (UserSettings.Default.UsernamesById is null) - { - UserSettings.Default.UsernamesById = new StringDictionarySetting(); - } - UserSettings.Default.UsernamesById[userId] = username; - UserSettings.Default.Save(); - } - } -} diff --git a/Morphic.Client/app.Development.manifest b/Morphic.Client/app.Development.manifest deleted file mode 100644 index 59e948e3..00000000 --- a/Morphic.Client/app.Development.manifest +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - PerMonitorV2 - - - - - - - diff --git a/Morphic.Client/app.Production.manifest b/Morphic.Client/app.Production.manifest deleted file mode 100644 index 59e948e3..00000000 --- a/Morphic.Client/app.Production.manifest +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - PerMonitorV2 - - - - - - - diff --git a/Morphic.Client/appsettings.Debug.json b/Morphic.Client/appsettings.Debug.json deleted file mode 100644 index 7de06dc3..00000000 --- a/Morphic.Client/appsettings.Debug.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "MorphicService": { - "ApiEndpointUrlAsString": "http://localhost:5002/", - "FrontEndUrlAsString": "http://localhost:8080/" - }, - "Update": { - "AppCastUrl": "" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.dev", - "AppKey": "b518ed6ca13aae3f60197dc5fdda2a0e649a54b3" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Development.canary.json b/Morphic.Client/appsettings.Development.canary.json deleted file mode 100644 index fe5dd0c3..00000000 --- a/Morphic.Client/appsettings.Development.canary.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic.org/", - "FrontEndUrlAsString": "https://app.morphic.org/", - "BarEditorWebAppUrlAsString": "https://custom.morphic.org/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows-canary.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Development.dev.json b/Morphic.Client/appsettings.Development.dev.json deleted file mode 100644 index ef04840b..00000000 --- a/Morphic.Client/appsettings.Development.dev.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic.dev/", - "FrontEndUrlAsString": "https://app.morphic.dev/", - "BarEditorWebAppUrlAsString": "https://custom.morphic.dev/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.dev/autoupdate/morphic-windows.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.dev", - "AppKey": "b518ed6ca13aae3f60197dc5fdda2a0e649a54b3" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Development.json b/Morphic.Client/appsettings.Development.json deleted file mode 100644 index fe5dd0c3..00000000 --- a/Morphic.Client/appsettings.Development.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic.org/", - "FrontEndUrlAsString": "https://app.morphic.org/", - "BarEditorWebAppUrlAsString": "https://custom.morphic.org/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows-canary.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Development.preview.json b/Morphic.Client/appsettings.Development.preview.json deleted file mode 100644 index a36fb31b..00000000 --- a/Morphic.Client/appsettings.Development.preview.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic.org/", - "FrontEndUrlAsString": "https://app.morphic.org/", - "BarEditorWebAppUrlAsString": "https://custom.morphic.org/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows-preview.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Production.json b/Morphic.Client/appsettings.Production.json deleted file mode 100644 index 1f120810..00000000 --- a/Morphic.Client/appsettings.Production.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic.org/", - "FrontEndUrlAsString": "https://app.morphic.org/", - "BarEditorWebAppUrlAsString": "https://custom.morphic.org/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/appsettings.Staging.json b/Morphic.Client/appsettings.Staging.json deleted file mode 100644 index 58e62fa5..00000000 --- a/Morphic.Client/appsettings.Staging.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information", - "CountlySDK": "Debug" - }, - "Console": { - "IncludeScopes": true, - "LogLevel": { - "CountlySDK": "Debug" - } - } - }, - "MorphicService": { - "ApiEndpointUrlAsString": "https://api.morphic-staging.com/", - "FrontEndUrlAsString": "https://app.morphic-staging.com/" - }, - "Update": { - "AppCastUrl": "https://app.morphic-staging.com/autoupdate/morphic-windows.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - }, - "Telemetry": { - "ServerHostname": "telemetrymqtt.morphic.org", - "AppName": "morphic-windows", - "AppKey": "IYvtOg4j5BcClFdT" - } -} \ No newline at end of file diff --git a/Morphic.Client/build-info.json b/Morphic.Client/build-info.json deleted file mode 100644 index a0f9a0c6..00000000 --- a/Morphic.Client/build-info.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "buildTime": "Mon, May 25, 2020 3:25:23 PM", - "commit": "" -} diff --git a/Morphic.Client/quickstrip.json b/Morphic.Client/quickstrip.json deleted file mode 100644 index dd88d543..00000000 --- a/Morphic.Client/quickstrip.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "items": [ - { - "type": "control", - "feature": "resolution" - }, - { - "type": "control", - "feature": "magnifier" - }, - { - "type": "control", - "feature": "snip" - }, - { - "type": "control", - "feature": "reader" - }, - { - "type": "control", - "feature": "colors" - } - ] -} diff --git a/Morphic.Client/solutions.json5 b/Morphic.Client/solutions.json5 deleted file mode 100644 index 6c912d8a..00000000 --- a/Morphic.Client/solutions.json5 +++ /dev/null @@ -1,106 +0,0 @@ -{ - solutionId: { - settings: [ - // settings - ], - platform: { - os: "windows", - ver: "123+" - } - }, - "com.microsoft.windows.magnifier": { - settings: [ - { - type: "process", - path: "magnify.exe", - settings: { - enabled: "isRunning" - } - }, - { - type: "registry", - path: "HKCU\\Software\\Microsoft\\ScreenMagnifier", - settings: { - magnification: { - name: "Magnification", - dataType: "int", - valueKind: "REG_DWORD" - } - } - } - ] - }, - "com.microsoft.windows.colorFilters": { - settings: [ - { - type: "systemSettings", - settings: { - enabled: "SystemSettings_Accessibility_ColorFiltering_IsEnabled:bool", - filterType: "SystemSettings_Accessibility_ColorFiltering_FilterType:int" - } - } - ] - }, - "com.microsoft.windows.highContrast": { - settings: [ - { - type: "themeSettings", - currentTheme: "${reg:HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\CurrentTheme}", - savedTheme: "${folder:LocalApplicationData}\\Microsoft\\Windows\\Themes\\Morphic.theme", - settings: { - enabled: "highContrastEnabled" - } - } - ] - }, - "com.microsoft.windows.narrator": { - settings: [ - { - type: "systemSettings", - settings: { - enabled: "SystemSettings_Accessibility_Narrator_IsEnabled:bool" - } - } - ] - }, - "com.microsoft.windows.display": { - settings: [ - { - type: "displaySettings", - settings: { - count: "zoomLevelCount", - zoom: { - name: "zoom", - dataType: "int", - local: true, - range: { - min: 0, - max: "count" - } - } - } - } - ] - }, - "com.microsoft.windows.nightMode": { - settings: [ - { - type: "systemSettings", - settings: { - enabled: "SystemSettings_Display_BlueLight_ManualToggleQuickAction:bool", - } - } - ] - }, - "com.microsoft.windows.lightTheme": { - settings: [ - { - type: "systemSettings", - settings: { - apps: "SystemSettings_Personalize_Color_AppsUseLightTheme:bool", - system: "SystemSettings_Personalize_Color_SystemUsesLightTheme:bool", - } - } - ] - } -} \ No newline at end of file diff --git a/Morphic.Client/test-bar.json5 b/Morphic.Client/test-bar.json5 deleted file mode 100644 index 852b1d06..00000000 --- a/Morphic.Client/test-bar.json5 +++ /dev/null @@ -1,363 +0,0 @@ -{ - id: "the id", - name: "not the name", - position: { - horizontal: true - }, - items: [ - { - kind: "application", - is_primary: true, - configuration: { - exe: "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App", - appx: true, - label: "calculator", - tooltipHeader: "tooltip header", - tooltip: "tooltip text" - } - }, - { - kind: "application", - is_primary: true, - configuration: { - exe: "notepad.exe", - label: "notepad 2", - tooltipHeader: "tooltip header only" - } - }, - { - kind: "application", - is_primary: true, - configuration: { - exe: "notepad.exe", - label: "notepad 3", - tooltip: "tooltip text only" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "screen-zoom", - label: "screen zoom" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "volume", - label: "Volume" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "magnify", - label: "Magnifier" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "nightmode", - label: "Night Mode" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "read-aloud", - label: "Read Aloud" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "copy-paste", - label: "Copy & Paste" - } - }, - { - kind: "action", - is_primary: true, - configuration: { - identifier: "high-contrast", - label: "High contrast" - } - }, - { - kind: "link", - is_primary: true, - configuration: { - label: "Google", - image_url: "test/grid", - url: "https://google.co.uk/" - } - }, - { - "kind": "action", - "is_primary": true, - "configuration": { - "label": "Screen Snip", - "identifier": "snip", - "color": "", - "image_url": "images-solid" - }, - "id": "10210050-take screenshot-generic-kind-79939245" - }, - { - kind: "link", - is_primary: true, - configuration: { - label: "Google Drive Home", - url: "https://drive.google.com/drive/my-drive" - } - }, - { - kind: "link", - is_primary: true, - configuration: { - label: "iCloud Files Home Folder aaa bbbb ccccc dd e ffff ggggg", - url: "https://www.icloud.com/iclouddrive/" - } - }, - { - kind: "link", - is_primary: true, - configuration: { - label: "Box Home Folder", - url: "https://www.box.com" - } - }, - { - kind: "link", - is_primary: true, - configuration: { - label: "One Drive Folder", - url: "https://onedrive.live.com/?id=root" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Wikipedia", - url: "https://wikipedia.com/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "BBC", - url: "https://bbc.co.uk/" - } - }, - - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "link", - is_primary: false, - configuration: { - label: "Morphic Home Page", - url: "https://morphic.org/" - } - }, - { - kind: "action", - is_primary: false, - configuration: { - identifier: "taskManager", - label: "open task manager" - } - }, - ] -} \ No newline at end of file diff --git a/Morphic.Controls/HybridTrayIcon.cs b/Morphic.Controls/HybridTrayIcon.cs new file mode 100644 index 00000000..6db7ca7b --- /dev/null +++ b/Morphic.Controls/HybridTrayIcon.cs @@ -0,0 +1,335 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Morphic.Controls; + +/// +/// Displays a system tray icon (NotifyIcon) in the notification area and/or an always-visible +/// button (Morphic.Controls.TrayButton.TrayButton) next to the notification area on the task bar. +/// +public class HybridTrayIcon : IDisposable +{ + private bool disposedValue; + + private System.Drawing.Icon? _icon = null; + private string? _text = null; + private bool _visible = false; + + // Used if a tray icon is desired instead of a next-to-tray taskbar button + private System.Windows.Forms.NotifyIcon? _notifyIcon = null; + + // Used if a next-to-tray button is desired instead of a tray icon + private Morphic.Controls.TrayButton.TrayButton? _trayButton = null; + + public enum TrayIconLocationOption + { + None, + NotificationTray, + NextToNotificationTray, + NotificationTrayAndNextToNotificationTray + } + // + private TrayIconLocationOption _trayIconLocation = TrayIconLocationOption.None; + + /// Raised when the button is clicked. + public event EventHandler? Click; + /// Raised when the button is right-clicked. + public event EventHandler? SecondaryClick; + + public HybridTrayIcon() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + _notifyIcon?.Dispose(); + _notifyIcon = null; + + _trayButton?.Dispose(); + _trayButton = null; + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// The icon for the tray icon + public System.Drawing.Icon? Icon + { + get + { + return _icon; + } + set + { + _icon = value; + if (_notifyIcon is not null) + { + _notifyIcon.Icon = _icon; + } + if (_trayButton is not null) + { + _trayButton.Icon = _icon; + } + } + } + + /// Tooltip for the tray icon. + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + if (_notifyIcon is not null) + { + _notifyIcon.Text = _text; + } + if (_trayButton is not null) + { + _trayButton.Text = _text; + } + } + } + + /// Show or hide the tray icon. + public bool Visible + { + get + { + return _visible; + } + set + { + _visible = value; + + if (_notifyIcon is not null) + { + _notifyIcon.Visible = _visible; + } + if (_trayButton is not null) + { + // NOTE: we set the visibility tag; if the tray button cannot currently be made visible, it will become ".PendingVisible" instead + _trayButton.Visibility = _visible ? TrayButton.TrayButtonVisibility.Visible : TrayButton.TrayButtonVisibility.Hidden; + } + } + } + + // + + public MorphicResult, MorphicUnit> GetPositionsAndSizes() + { + var result = new List(); + + if (_notifyIcon is not null) + { + // NOTE: if we can find the exact position and size of the icon in the system notification tray, we should return that value instead + // NOTE: since we cannot currently and reliably return a list of actual positions+sizes in this circumstance, we return an error + return MorphicResult.ErrorResult(); + } + + if (_trayButton is not null) + { + var positionAndSize = _trayButton.PositionAndSize; + if (positionAndSize.HasValue == true) + { + result.Add(positionAndSize.Value); + } + } + + return MorphicResult.OkResult(result); + } + + // NOTE: this function returns just the first position and size (i.e. rect) of our tray icon; if it cannot find at least one position+size, it returns null + public System.Windows.Rect? GetPositionAndSizeOrNull() + { + var getPositionsAndSizesResult = this.GetPositionsAndSizes(); + if (getPositionsAndSizesResult.IsSuccess) + { + var positionsAndSizes = getPositionsAndSizesResult.Value!; + if (positionsAndSizes.Count == 1) + { + var positionAndSize = positionsAndSizes[0]; + return new System.Windows.Rect(positionAndSize.X, positionAndSize.Y, positionAndSize.Width, positionAndSize.Height); + } + else + { + Debug.Assert(false, "Could not get positions and sizes of tray icon(s); this is to be expected if we cannot capture the rectangle (which may be the case if we're putting the icon in the system tray itself)"); + } + } + + return null; + } + + // + + private void InitializeTrayIcon() + { + if (_notifyIcon is not null) + { + return; + } + + _notifyIcon = new System.Windows.Forms.NotifyIcon(); + _notifyIcon.Text = _text; + _notifyIcon.Icon = _icon; + // + _notifyIcon.MouseUp += (sender, args) => + { + if (args.Button == System.Windows.Forms.MouseButtons.Right) + { + this.SecondaryClick?.Invoke(this, args); + } + else if (args.Button == System.Windows.Forms.MouseButtons.Left) + { + this.Click?.Invoke(this, args); + } + }; + _notifyIcon.Visible = _visible; + } + + private void InitializeTrayButton() + { + if (_trayButton is not null) + { + // if we are being initialized a second time, simply return success + return; + } + + _trayButton = new Morphic.Controls.TrayButton.TrayButton(); + _trayButton.Text = _text; + _trayButton.Icon = _icon; + // + _trayButton.MouseUp += (sender, args) => + { + if (args.Button == System.Windows.Forms.MouseButtons.Right) + { + this.SecondaryClick?.Invoke(this, args); + } + else if (args.Button == System.Windows.Forms.MouseButtons.Left) + { + this.Click?.Invoke(this, args); + } + }; + // + // NOTE: we set the visibility tag; if the tray button cannot currently be made visible, it will become ".PendingVisible" instead + _trayButton.Visibility = _visible ? TrayButton.TrayButtonVisibility.Visible : TrayButton.TrayButtonVisibility.Hidden; + } + + // + + public TrayIconLocationOption TrayIconLocation + { + get + { + return _trayIconLocation; + } + set + { + _trayIconLocation = value; + + // create notify icon if requested + switch (value) + { + case TrayIconLocationOption.NotificationTray: + case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: + if (_notifyIcon is null) + { + this.InitializeTrayIcon(); + } + break; + } + + // create tray button if requested + switch (value) + { + case TrayIconLocationOption.NextToNotificationTray: + case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: + if (_trayButton is null) + { + this.InitializeTrayButton(); + } + break; + } + + // destroy notify icon if no longer wanted + switch (value) + { + case TrayIconLocationOption.None: + case TrayIconLocationOption.NextToNotificationTray: + if (_notifyIcon is not null) + { + _notifyIcon.Dispose(); + _notifyIcon = null; + } + break; + } + + // destroy tray button if no longer wanted + switch (value) + { + case TrayIconLocationOption.None: + case TrayIconLocationOption.NotificationTray: + if (_trayButton is not null) + { + _trayButton.Dispose(); + _trayButton = null; + } + break; + } + } + } +} diff --git a/Morphic.Controls/Morphic.Controls.csproj b/Morphic.Controls/Morphic.Controls.csproj new file mode 100644 index 00000000..708e24b4 --- /dev/null +++ b/Morphic.Controls/Morphic.Controls.csproj @@ -0,0 +1,41 @@ + + + + net8.0-windows10.0.22621.0 + 10.0.19041.0 + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + enable + true + true + + + + + all + + + + + + + + + + + $(DefineConstants);PLATFORM_X86 + + + + + + + + $(DefineConstants);PLATFORM_X86 + + + + + + + diff --git a/Morphic.Controls/NativeMethods.txt b/Morphic.Controls/NativeMethods.txt new file mode 100644 index 00000000..36dd3977 --- /dev/null +++ b/Morphic.Controls/NativeMethods.txt @@ -0,0 +1,52 @@ +BeginBufferedPaint +BeginPaint +BringWindowToTop +BufferedPaintClear +BufferedPaintInit +BufferedPaintUnInit +CreateCompatibleDC +CreateSolidBrush +CreateWindowEx +DeleteDC +DeleteObject +DestroyIcon +DestroyWindow +EndBufferedPaint +EndPaint +FillRect +FindWindow +FindWindowEx +GetClientRect +GetDC +GetDesktopWindow +GetWindow +GetWindowLong +GetWindowRect +LoadCursor +ReleaseDC +SelectObject +SendMessage +SetLayeredWindowAttributes +SetWindowLong +SetWindowLongPtr +SetWindowPos +UpdateLayeredWindow + +BLENDFUNCTION +PAINTSTRUCT +RECT + +REDRAW_WINDOW_FLAGS +WIN32_ERROR +WINDOW_EX_STYLE +WINDOW_STYLE +WPARAM + +AC_SRC_ALPHA +AC_SRC_OVER +HWND_TOPMOST +IDC_ARROW +S_OK +TTM_ADDTOOL +TTM_DELTOOL +TTS_ALWAYSTIP diff --git a/Morphic.Controls/PInvokeExtensions.cs b/Morphic.Controls/PInvokeExtensions.cs new file mode 100644 index 00000000..06b98efd --- /dev/null +++ b/Morphic.Controls/PInvokeExtensions.cs @@ -0,0 +1,326 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Morphic.Controls; + +internal class PInvokeExtensions +{ + #region commctrl + + internal const string TOOLTIPS_CLASS = "tooltips_class32"; + // + internal const byte TTS_ALWAYSTIP = 0x01; + //internal const byte TTS_NOPREFIX = 0x02; + //internal const byte TTS_BALLOON = 0x40; + internal const ushort TTF_SUBCLASS = 0x0010; + // + internal const ushort TTM_ADDTOOL = WM_USER + 50; + internal const ushort TTM_DELTOOL = WM_USER + 51; + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-tttoolinfow +#pragma warning disable CS0649 // NOTE: hinst and lParam may never be written to (and will remain as IntPtr.Zero) in this implementation + internal struct TOOLINFO + { + public uint cbSize; + public uint uFlags; + public IntPtr hwnd; + public UIntPtr uId; + public Windows.Win32.Foundation.RECT rect; + public IntPtr hinst; + [MarshalAs(UnmanagedType.LPTStr)] + public string? lpszText; + public IntPtr lParam; + //public IntPtr reserved; // NOTE: this exists in the official declaration as a void pointer but adding it causes SendMessage to fail; pinvoke.net leaves it out and so do we + } +#pragma warning restore CS0649 // NOTE: hinst and lParam may never be written to (and will remain as IntPtr.Zero) in this implementation + + #endregion commctrl + + #region winuser + + internal const int CW_USEDEFAULT = unchecked((int)0x80000000); + + internal const ushort MK_LBUTTON = 0x0001; + internal const ushort MK_RBUTTON = 0x0002; + + internal const ushort WM_USER = 0x0400; + + // + + internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent + [Flags] + internal enum TRACKMOUSEEVENTFlags : uint + { + TME_CANCEL = 0x80000000, + TME_HOVER = 0x00000001, + TME_LEAVE = 0x00000002, + TME_NONCLIENT = 0x00000010, + TME_QUERY = 0x40000000 + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook + [Flags] + internal enum WinEventHookFlags : uint + { + WINEVENT_OUTOFCONTEXT = 0x0000, // Events are ASYNC + WINEVENT_SKIPOWNTHREAD = 0x0001, // Don't call back for events on installer's thread + WINEVENT_SKIPOWNPROCESS = 0x0002, // Don't call back for events on installer's process + WINEVENT_INCONTEXT = 0x0004, // Events are SYNC, this causes your dll to be injected into every process + } + + // https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants + public enum WinEventHookType : uint + { + EVENT_AIA_START = 0xA000, + EVENT_AIA_END = 0xAFFF, + EVENT_MIN = 0x00000001, + EVENT_MAX = 0x7FFFFFFF, + EVENT_OBJECT_ACCELERATORCHANGE = 0x8012, + EVENT_OBJECT_CLOAKED = 0x8017, + EVENT_OBJECT_CONTENTSCROLLED = 0x8015, + EVENT_OBJECT_CREATE = 0x8000, + EVENT_OBJECT_DEFACTIONCHANGE = 0x8011, + EVENT_OBJECT_DESCRIPTIONCHANGE = 0x800D, + EVENT_OBJECT_DESTROY = 0x8001, + EVENT_OBJECT_DRAGSTART = 0x8021, + EVENT_OBJECT_DRAGCANCEL = 0x8022, + EVENT_OBJECT_DRAGCOMPLETE = 0x8023, + EVENT_OBJECT_DRAGENTER = 0x8024, + EVENT_OBJECT_DRAGLEAVE = 0x8025, + EVENT_OBJECT_DRAGDROPPED = 0x8026, + EVENT_OBJECT_END = 0x80FF, + EVENT_OBJECT_FOCUS = 0x8005, + EVENT_OBJECT_HELPCHANGE = 0x8010, + EVENT_OBJECT_HIDE = 0x8003, + EVENT_OBJECT_HOSTEDOBJECTSINVALIDATED = 0x8020, + EVENT_OBJECT_IME_HIDE = 0x8028, + EVENT_OBJECT_IME_SHOW = 0x8027, + EVENT_OBJECT_IME_CHANGE = 0x8029, + EVENT_OBJECT_INVOKED = 0x8013, + EVENT_OBJECT_LIVEREGIONCHANGED = 0x8019, + EVENT_OBJECT_LOCATIONCHANGE = 0x800B, + EVENT_OBJECT_NAMECHANGE = 0x800C, + EVENT_OBJECT_PARENTCHANGE = 0x800F, + EVENT_OBJECT_REORDER = 0x8004, + EVENT_OBJECT_SELECTION = 0x8006, + EVENT_OBJECT_SELECTIONADD = 0x8007, + EVENT_OBJECT_SELECTIONREMOVE = 0x8008, + EVENT_OBJECT_SELECTIONWITHIN = 0x8009, + EVENT_OBJECT_SHOW = 0x8002, + EVENT_OBJECT_STATECHANGE = 0x800A, + EVENT_OBJECT_TEXTEDIT_CONVERSIONTARGETCHANGED = 0x8030, + EVENT_OBJECT_TEXTSELECTIONCHANGED = 0x8014, + EVENT_OBJECT_UNCLOAKED = 0x8018, + EVENT_OBJECT_VALUECHANGE = 0x800E, + EVENT_OEM_DEFINED_START = 0x0101, + EVENT_OEM_DEFINED_END = 0x01FF, + EVENT_SYSTEM_ALERT = 0x0002, + EVENT_SYSTEM_ARRANGMENTPREVIEW = 0x8016, + EVENT_SYSTEM_CAPTUREEND = 0x0009, + EVENT_SYSTEM_CAPTURESTART = 0x0008, + EVENT_SYSTEM_CONTEXTHELPEND = 0x000D, + EVENT_SYSTEM_CONTEXTHELPSTART = 0x000C, + EVENT_SYSTEM_DESKTOPSWITCH = 0x0020, + EVENT_SYSTEM_DIALOGEND = 0x0011, + EVENT_SYSTEM_DIALOGSTART = 0x0010, + EVENT_SYSTEM_DRAGDROPEND = 0x000F, + EVENT_SYSTEM_DRAGDROPSTART = 0x000E, + EVENT_SYSTEM_END = 0x00FF, + EVENT_SYSTEM_FOREGROUND = 0x0003, + EVENT_SYSTEM_MENUPOPUPEND = 0x0007, + EVENT_SYSTEM_MENUPOPUPSTART = 0x0006, + EVENT_SYSTEM_MENUEND = 0x0005, + EVENT_SYSTEM_MENUSTART = 0x0004, + EVENT_SYSTEM_MINIMIZEEND = 0x0017, + EVENT_SYSTEM_MINIMIZESTART = 0x0016, + EVENT_SYSTEM_MOVESIZEEND = 0x000B, + EVENT_SYSTEM_MOVESIZESTART = 0x000A, + EVENT_SYSTEM_SCROLLINGEND = 0x0013, + EVENT_SYSTEM_SCROLLINGSTART = 0x0012, + EVENT_SYSTEM_SOUND = 0x0001, + EVENT_SYSTEM_SWITCHEND = 0x0015, + EVENT_SYSTEM_SWITCHSTART = 0x0014, + EVENT_UIA_EVENTID_START = 0x4E00, + EVENT_UIA_EVENTID_END = 0x4EFF, + EVENT_UIA_PROPID_START = 0x7500, + EVENT_UIA_PROPID_END = 0x75FF + } + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent + [StructLayout(LayoutKind.Sequential)] + internal struct TRACKMOUSEEVENT + { + public uint cbSize; + public TRACKMOUSEEVENTFlags dwFlags; + public IntPtr hWnd; + public uint dwHoverTime; + + public static TRACKMOUSEEVENT CreateNew(TRACKMOUSEEVENTFlags dwFlags, IntPtr hWnd, uint dwHoverTime) + { + var result = new TRACKMOUSEEVENT() + { + cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)), + dwFlags = dwFlags, + hWnd = hWnd, + dwHoverTime = dwHoverTime + }; + return result; + } + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WNDCLASSEX + { + public uint cbSize; + public uint style; + public IntPtr lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + public string? lpszMenuName; + public string? lpszClassName; // NOTE: this member should be initialized (i.e. non-null) + public IntPtr hIconSm; + + public static WNDCLASSEX CreateNew() + { + var result = new WNDCLASSEX() + { + cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEX)) + }; + return result; + } + } + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wineventproc + internal delegate void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc + internal delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + // + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CreateWindowEx( + PInvoke.User32.WindowStylesEx dwExStyle, + IntPtr lpClassName, + string? lpWindowName, + PInvoke.User32.WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclassnamew + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw + internal static IntPtr GetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex) + { + if (IntPtr.Size == 4) + { + return (nint)Windows.Win32.PInvoke.GetWindowLong(hWnd, nIndex); + } + else + { + return PInvokeExtensions.GetWindowLongPtr(hWnd.Value, nIndex); + } + } + // + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex); + + // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints + // NOTE: this signature is the POINT option (in which cPoints must always be set to 1). + [DllImport("user32.dll", SetLastError = true)] + internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.POINT lpPoints, uint cPoints); + // + // see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints + // NOTE: this signature is the RECT option (in which cPoints must always be set to 2). + [DllImport("user32.dll", SetLastError = true)] + internal static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref PInvoke.RECT lpPoints, uint cPoints); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-redrawwindow + [DllImport("user32.dll")] + internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS flags); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassexw + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern ushort RegisterClassEx([In] ref WNDCLASSEX lpWndClass); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptrw + internal static IntPtr SetWindowLongPtr_IntPtr(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX nIndex, IntPtr dwNewLong) + { +#if PLATFORM_X86 + return (nint)Windows.Win32.PInvoke.SetWindowLong(hWnd, nIndex, (int)dwNewLong); +#else + if (IntPtr.Size == 4) + { + return (nint)Windows.Win32.PInvoke.SetWindowLong(hWnd, nIndex, (int)dwNewLong); + } + else + { + return Windows.Win32.PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong); + } +#endif + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook + [DllImport("user32.dll")] + internal static extern IntPtr SetWinEventHook(WinEventHookType eventMin, WinEventHookType eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, WinEventHookFlags dwFlags); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackmouseevent + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unhookwinevent + [DllImport("user32.dll")] + internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); + + #endregion winuser +} diff --git a/Morphic.Controls/TrayButton/TrayButton.cs b/Morphic.Controls/TrayButton/TrayButton.cs new file mode 100644 index 00000000..c446e344 --- /dev/null +++ b/Morphic.Controls/TrayButton/TrayButton.cs @@ -0,0 +1,270 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; + +namespace Morphic.Controls.TrayButton; + +public class TrayButton : IDisposable +{ + private bool disposedValue; + + // NOTE: only one of the two tray button variants will be populated (i.e. based on the OS version) + // [we have chosen not to create a common interface between them, as the plan is to deprecate the Windows 10 variant once Windows 10 is no longer supported...and the Windows 11+ variant should be allowed to get a new API surface if/as needed] + Morphic.Controls.TrayButton.Windows10.TrayButton? _legacyTrayButton; + Morphic.Controls.TrayButton.Windows11.TrayButton? _trayButton; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + public System.Drawing.Rectangle? PositionAndSize + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + // Windows 11 and newer (i.e. modern tray button) + return _trayButton?.PositionAndSize; + } + else + { + // Windows 10 (i.e. legacy tray button) + return _legacyTrayButton?.PositionAndSize; + } + } + } + + public TrayButton() + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + // Windows 11 and newer (i.e. modern tray button) + _trayButton = new(); + _trayButton.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + } + else + { + // Windows 10 (i.e. legacy tray button) + _legacyTrayButton = new(); + _legacyTrayButton.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + // + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + // Windows 11 and newer (i.e. modern tray button) + _trayButton?.Dispose(); + } + else + { + // Windows 10 (i.e. legacy tray button) + _legacyTrayButton?.Dispose(); + } + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~TrayButton() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public System.Drawing.Bitmap? Bitmap + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Bitmap; + } + else //if (.IsWindows10() == true) + { + var icon = _legacyTrayButton!.Icon; + return (icon is not null) ? icon!.ToBitmap() : null; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Bitmap = value; + } + else //if (.IsWindows10() == true) + { + if (value is not null) + { + var bitmapAsIconHandlePointer = value.GetHicon(); + try + { + _legacyTrayButton!.Icon = (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); + } + finally + { + _ = Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); + } + } + else + { + _legacyTrayButton!.Icon = null; + } + } + } + } + + public System.Drawing.Icon? Icon + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + if (_trayButton!.Bitmap is not null) + { + var bitmapAsIconHandlePointer = _trayButton!.Bitmap!.GetHicon(); + try + { + return (System.Drawing.Icon)(System.Drawing.Icon.FromHandle(bitmapAsIconHandlePointer).Clone()); + } + finally + { + Windows.Win32.PInvoke.DestroyIcon((Windows.Win32.UI.WindowsAndMessaging.HICON)bitmapAsIconHandlePointer); + } + } + else + { + return null; + } + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Icon; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Bitmap = (value is not null) ? value!.ToBitmap() : null; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Icon = value; + } + } + } + + public string? Text + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Text; + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Text; + } + } + set + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Text = value; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Text = value; + } + } + } + + public TrayButtonVisibility Visibility + { + get + { + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + return _trayButton!.Visibility; + } + else //if (.IsWindows10() == true) + { + return _legacyTrayButton!.Visible switch + { + true => TrayButtonVisibility.Visible, + false => TrayButtonVisibility.Hidden, + }; + } + } + set + { + var newVisibleState = value switch + { + TrayButtonVisibility.Hidden => false, + TrayButtonVisibility.PendingVisible => throw new ArgumentException("State 'PendingVisible' is invalid for the Visibility Set operation"), + TrayButtonVisibility.Visible => true, + _ => throw new Exception("invalid code path"), + }; + + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _trayButton!.Visibility = value; + } + else //if (.IsWindows10() == true) + { + _legacyTrayButton!.Visible = newVisibleState; + } + } + } +} diff --git a/Morphic.Windows.Native/Ini/Lexer/IniLineTerminatorOption.cs b/Morphic.Controls/TrayButton/TrayButtonVisibility.cs similarity index 64% rename from Morphic.Windows.Native/Ini/Lexer/IniLineTerminatorOption.cs rename to Morphic.Controls/TrayButton/TrayButtonVisibility.cs index 99714fe1..398ae6d8 100644 --- a/Morphic.Windows.Native/Ini/Lexer/IniLineTerminatorOption.cs +++ b/Morphic.Controls/TrayButton/TrayButtonVisibility.cs @@ -1,10 +1,10 @@ -// Copyright 2020-2021 Raising the Floor - International +// Copyright 2020-2025 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. // // You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt // // The R&D leading to these results received funding from the: // * Rehabilitation Services Administration, US Dept. of Education under @@ -21,19 +21,11 @@ // * Adobe Foundation // * Consumer Electronics Association Foundation -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +namespace Morphic.Controls.TrayButton; -namespace Morphic.Windows.Native.Ini +public enum TrayButtonVisibility { - internal enum IniLineTerminatorOption - { - None, - Cr, - CrLf, - Lf - } + Hidden, // currently hidden + PendingVisible, // is not yet visible; NOTE: this usually occurs if the visible state has been set to true but the tray button does not currently have a taskbar location (or taskbar) in/over which to position itself + Visible, // currently visible } diff --git a/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs b/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs new file mode 100644 index 00000000..6b4cdd30 --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows10/LegacyWindowsApi.cs @@ -0,0 +1,629 @@ +// Copyright 2020-2024 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; +using System.Runtime.InteropServices; + +namespace Morphic.Controls.TrayButton.Windows10; + +internal class LegacyWindowsApi +{ + #region Win32 error codes + + public const uint ERROR_SUCCESS = 0; + + #endregion + + #region Window Positioning + +#pragma warning disable CS0660 // Code should override Object.Equals(object o) when defining == and != operations, but the legacy struct RECT does not. +#pragma warning disable CS0661 // Code should override Object.GetHashCode() when defining == and != operations, but the legacy struct RECT does not. + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public PInvoke.RECT ToPInvokeRect() + { + return new PInvoke.RECT() { left = this.Left, top = this.Top, right = this.Right, bottom = this.Bottom }; + } + + /// + /// Creates a win32 RECT from a .NET Rect. + /// + /// The rectangle. + public RECT(System.Windows.Rect rect) + { + this.Left = (int)rect.Left; + this.Top = (int)rect.Top; + this.Right = (int)rect.Right; + this.Bottom = (int)rect.Bottom; + } + + public static RECT Empty + { + get + { + return new RECT(new System.Windows.Rect(0, 0, 0, 0)); + } + } + + public bool HasNonZeroWidthOrHeight() + { + return ((this.Left == this.Right) || (this.Top == this.Bottom)); + } + + public bool IsInside(RECT rect) + { + return ((this.Left >= rect.Left) && (this.Right <= rect.Right) && (this.Top >= rect.Top) && (this.Bottom <= rect.Bottom)); + } + + public bool Intersects(RECT rect) + { + bool overlapsHorizontally = false; + bool overlapsVertically = false; + + // horizontal check + if ((this.Right > rect.Left) && (this.Left < rect.Right)) + { + // partially or fully overlaps horizontally + overlapsHorizontally = true; + } + + // vertical check + if ((this.Bottom > rect.Top) && (this.Top < rect.Bottom)) + { + // partially or fully overlaps vertically + overlapsVertically = true; + } + + if ((overlapsHorizontally == true) && (overlapsVertically == true)) + { + return true; + } + + // if we could not find overlap, then return false + return false; + } + + public static bool operator ==(RECT lhs, RECT rhs) + { + if ((lhs.Left == rhs.Left) && + (lhs.Top == rhs.Top) && + (lhs.Right == rhs.Right) && + (lhs.Bottom == rhs.Bottom)) + { + return true; + } + else + { + return false; + } + } + + public static bool operator !=(RECT lhs, RECT rhs) + { + if ((lhs.Left != rhs.Left) || + (lhs.Top != rhs.Top) || + (lhs.Right != rhs.Right) || + (lhs.Bottom != rhs.Bottom)) + { + return true; + } + else + { + return false; + } + } + } +#pragma warning restore CS0660 // Code should override Object.Equals(object o) when defining == and != operations, but the legacy struct RECT does not. +#pragma warning restore CS0661 // Code should override Object.GetHashCode() when defining == and != operations, but the legacy struct RECT does not. + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + + public POINT(System.Windows.Point pt) : this((int)pt.X, (int)pt.Y) + { + } + + public System.Windows.Point ToPoint() + { + return new System.Windows.Point(this.X, this.Y); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct + [StructLayout(LayoutKind.Sequential)] + internal struct MSLLHOOKSTRUCT + { + public PInvoke.POINT pt; + // NOTE: the mouseData DWORD is apparently used as a signed integer (rather than as a uint) + public int mouseData; + public uint flags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [DllImport("user32.dll")] + internal static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll")] + public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + + #endregion + + #region Window Creation and Management + + [DllImport("gdi32.dll")] + internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref BITMAPINFO pbmi, uint usage, out IntPtr ppvBits, IntPtr hSection, uint offset); + + //[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + //internal static extern IntPtr CreateWindowEx( + // WindowStylesEx dwExStyle, + // IntPtr lpClassName, + // string? lpWindowName, + // WindowStyles dwStyle, + // int x, + // int y, + // int nWidth, + // int nHeight, + // IntPtr hWndParent, + // IntPtr hMenu, + // IntPtr hInstance, + // IntPtr lpParam); + // + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr CreateWindowEx( + WindowStylesEx dwExStyle, + string lpClassName, + string? lpWindowName, + WindowStyles dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("gdi32.dll")] + internal static extern bool DeleteDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + internal static extern bool DeleteObject(IntPtr ho); + + [DllImport("user32.dll")] + internal static extern bool DestroyWindow(IntPtr hWnd); + + internal delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumChildWindows(IntPtr hwndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpszClass, string? lpszWindow); + + //[DllImport("user32.dll")] + //internal static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); + // + internal enum Cursors + { + IDC_APPSTARTING = 32650, + IDC_ARROW = 32512, + IDC_CROSS = 32515, + IDC_HAND = 32649, + IDC_HELP = 32651, + IDC_IBEAM = 32513, + IDC_ICON = 32641, + IDC_NO = 32648, + IDC_SIZE = 32640, + IDC_SIZEALL = 32646, + IDC_SIZENESW = 32643, + IDC_SIZENS = 32645, + IDC_SIZENWSE = 32642, + IDC_SIZEWE = 32644, + IDC_UPARROW = 32516, + IDC_WAIT = 32514, + } + + [DllImport("user32.dll")] + internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags); + + // source for values: http://www.pinvoke.net/default.aspx/Enums/RedrawWindowFlags.html + internal enum RedrawWindowFlags : uint + { + // invalidation flags + RDW_ERASE = 0x4, + RDW_FRAME = 0x400, + RDW_INTERNALPAINT = 0x2, + RDW_INVALIDATE = 0x1, + // validation flags + RDW_NOERASE = 0x20, + RDW_NOFRAME = 0x800, + RDW_NOINTERNALPAINT = 0x10, + RDW_VALIDATE = 0x8, + // repainting flags + RDW_ERASENOW = 0x200, + RDW_UPDATENOW = 0x100, + // misc. control flags + RDW_ALLCHILDREN = 0x80, + RDW_NOCHILDREN = 0x40 + } + + [DllImport("gdi32.dll")] + internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr h); + + [DllImport("user32.dll")] + internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags); + + internal enum SetWindowPosFlags : uint + { + SWP_ASYNCWINDOWPOS = 0x4000, + SWP_DEFERERASE = 0x2000, + SWP_DRAWFRAME = 0x0020, + SWP_FRAMECHANGED = 0x0020, + SWP_HIDEWINDOW = 0x0080, + SWP_NOACTIVATE = 0x0010, + SWP_NOCOPYBITS = 0x0100, + SWP_NOMOVE = 0x0002, + SWP_NOOWNERZORDER = 0x0200, + SWP_NOREDRAW = 0x0008, + SWP_NOREPOSITION = 0x0200, + SWP_NOSENDCHANGING = 0x0400, + SWP_NOSIZE = 0x0001, + SWP_NOZORDER = 0x0004, + SWP_SHOWWINDOW = 0x0040 + } + + [DllImport("user32.dll")] + internal static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); + + [StructLayout(LayoutKind.Sequential)] + internal struct TRACKMOUSEEVENT + { + public uint cbSize; + public TMEFlags dwFlags; + public IntPtr hWnd; + public uint dwHoverTime; + + public TRACKMOUSEEVENT(TMEFlags dwFlags, IntPtr hWnd, uint dwHoverTime) + { + this.cbSize = (uint)Marshal.SizeOf(typeof(TRACKMOUSEEVENT)); + this.dwFlags = dwFlags; + this.hWnd = hWnd; + this.dwHoverTime = dwHoverTime; + } + } + + // WinUser.h (Windows 10 1809 SDK) + internal static readonly uint HOVER_DEFAULT = 0xFFFFFFFF; + + [Flags] + internal enum TMEFlags : uint + { + TME_CANCEL = 0x80000000, + TME_HOVER = 0x00000001, + TME_LEAVE = 0x00000002, + TME_NONCLIENT = 0x00000010, + TME_QUERY = 0x40000000 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct BITMAPINFO + { + public BITMAPINFOHEADER bmiHeader; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // NOTE: in other implementations, this was represented as a uint instead (with 256 elements instead of 1 element) + public RGBQUAD[] bmiColors; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BITMAPINFOHEADER + { + public uint biSize; + public int biWidth; + public int biHeight; + public ushort biPlanes; + public ushort biBitCount; + public BitmapCompressionType biCompression; + public uint biSizeImage; + public int biXPelsPerMeter; + public int biYPelsPerMeter; + public uint biClrUsed; + public uint biClrImportant; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RGBQUAD + { + byte rgbBlue; + byte rgbGreen; + byte rgbRed; + byte rgbReserved; + } + + // wingdi.h (Windows 10 1809 SDK) + internal enum BitmapCompressionType : uint + { + BI_RGB = 0, + BI_RLE8 = 1, + BI_RLE4 = 2, + BI_BITFIELDS = 3, + BI_JPEG = 4, + BI_PNG = 5 + } + + internal static readonly IntPtr HWND_TOP = new IntPtr(0); + //internal static readonly IntPtr HWND_BOTTOM = new IntPtr(1); + //internal static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + //internal static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2); + + internal const int MA_NOACTIVATEANDEAT = 4; + + // + + internal const uint MK_LBUTTON = 0x0001; + internal const uint MK_RBUTTON = 0x0002; + + internal const uint S_OK = 0; + + // WinUser.h (Windows 10 1809 SDK) + public enum WindowMessage : uint + { + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-create + WM_CREATE = 0x0001, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-destroy + WM_DESTROY = 0x0002, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-displaychange + WM_DISPLAYCHANGE = 0x007E, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-erasebkgnd + WM_ERASEBKGND = 0x0014, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown + WM_LBUTTONDOWN = 0x0201, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup + WM_LBUTTONUP = 0x0202, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate + WM_MOUSEACTIVATE = 0x0021, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseleave + WM_MOUSELEAVE = 0x02A3, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove + WM_MOUSEMOVE = 0x0200, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest + WM_NCHITTEST = 0x0084, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-ncpaint + WM_NCPAINT = 0x0085, + + // https://docs.microsoft.com/en-us/windows/win32/gdi/wm-paint + WM_PAINT = 0x000F, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged + WM_WINDOWPOSCHANGED = 0x0047, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanging + WM_WINDOWPOSCHANGING = 0x0046, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondown + WM_RBUTTONDOWN = 0x0204, + + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttonup + WM_RBUTTONUP = 0x0205, + + // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-setcursor + WM_SETCURSOR = 0x0020, + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-size + WM_SIZE = 0x0005, + } + + [Flags] + internal enum WindowStyles : uint + { + WS_BORDER = 0x00800000, + WS_CAPTION = 0x00C00000, + WS_CHILD = 0x40000000, + WS_CHILDWINDOW = 0x40000000, + WS_CLIPCHILDREN = 0x02000000, + WS_CLIPSIBLINGS = 0x04000000, + WS_DISABLED = 0x08000000, + WS_DLGFRAME = 0x00400000, + WS_GROUP = 0x00020000, + WS_HSCROLL = 0x00100000, + WS_ICONIC = 0x20000000, + WS_MAXIMIZE = 0x01000000, + WS_MAXIMIZEBOX = 0x00010000, + WS_MINIMIZE = 0x20000000, + WS_MINIMIZEBOX = 0x00020000, + WS_OVERLAPPED = 0x00000000, + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_POPUP = 0x80000000, + WS_POPUPWINDOW = WS_POPUP | WS_BORDER | WS_SYSMENU, + WS_SIZEBOX = 0x00040000, + WS_SYSMENU = 0x00080000, + WS_TABSTOP = 0x00010000, + WS_THICKFRAME = 0x00040000, + WS_TILED = 0x00000000, + WS_TILEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX, + WS_VISIBLE = 0x10000000, + WS_VSCROLL = 0x00200000 + } + + [Flags] + internal enum WindowStylesEx : uint + { + WS_EX_ACCEPTFILES = 0x00000010, + WS_EX_APPWINDOW = 0x00040000, + WS_EX_CLIENTEDGE = 0x00000200, + WS_EX_COMPOSITED = 0x02000000, + WS_EX_CONTEXTHELP = 0x00000400, + WS_EX_CONTROLPARENT = 0x00010000, + WS_EX_DLGMODALFRAME = 0x00000001, + WS_EX_LAYERED = 0x00080000, + WS_EX_LAYOUTRTL = 0x00400000, + WS_EX_LEFT = 0x00000000, + WS_EX_LEFTSCROLLBAR = 0x00004000, + WS_EX_LTRREADING = 0x00000000, + WS_EX_MDICHILD = 0x00000040, + WS_EX_NOACTIVATE = 0x08000000, + WS_EX_NOINHERITLAYOUT = 0x00100000, + WS_EX_NOPARENTNOTIFY = 0x00000004, + WS_EX_NOREDIRECTIONBITMAP = 0x00200000, + WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE, + WS_EX_PALETTEWINDOW = WS_EX_WINDOWEDGE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST, + WS_EX_RIGHT = 0x00001000, + WS_EX_RIGHTSCROLLBAR = 0x00000000, + WS_EX_RTLREADING = 0x00002000, + WS_EX_STATICEDGE = 0x00020000, + WS_EX_TOOLWINDOW = 0x00000080, + WS_EX_TOPMOST = 0x00000008, + WS_EX_TRANSPARENT = 0x00000020, + WS_EX_WINDOWEDGE = 0x00000100 + } + + #endregion + + #region Window Painting + + // NOTE: per pinvoke.net, this function is called "GdiAlphaBlend" even though the Microsoft documentation calls it AlphaBlend + [DllImport("gdi32.dll", EntryPoint = "GdiAlphaBlend")] + internal static extern bool AlphaBlend(IntPtr hdcDest, int xOriginDest, int yOriginDest, int wDest, int hDest, IntPtr hdcSrc, int xOriginSrc, int yOriginSrc, int wSrc, int hSrc, BLENDFUNCTION ftn); + + [StructLayout(LayoutKind.Sequential)] + public struct BLENDFUNCTION + { + public byte BlendOp; + public byte BlendFlags; + public byte SourceConstantAlpha; + public byte AlphaFormat; + } + + internal const byte AC_SRC_OVER = 0x00; + //internal const byte AC_SRC_ALPHA = 0x01; + + internal const uint DIB_RGB_COLORS = 0; + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-beginbufferedpaint + [DllImport("uxtheme.dll")] + internal static extern IntPtr BeginBufferedPaint(IntPtr hdcTarget, [In] ref RECT prcTarget, BP_BUFFERFORMAT dwFormat, IntPtr pPaintParams, out IntPtr phdc); + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/ne-uxtheme-bp_bufferformat + internal enum BP_BUFFERFORMAT : uint + { + BPBF_COMPATIBLEBITMAP, + BPBF_DIB, + BPBF_TOPDOWNDIB, + BPBF_TOPDOWNMONODIB + } + + [DllImport("user32.dll")] + internal static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintclear + [DllImport("uxtheme.dll")] + internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, ref RECT prc); + // + [DllImport("uxtheme.dll")] + internal static extern uint BufferedPaintClear(IntPtr hBufferedPaint, IntPtr prc); + + [DllImport("uxtheme.dll")] + internal static extern int BufferedPaintInit(); + + [DllImport("uxtheme.dll")] + internal static extern int BufferedPaintUnInit(); + + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawiconex + [DllImport("user32.dll")] + internal static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, uint istepIfAniCur, IntPtr hbrFlickerFreeDraw, DrawIconFlags diFlags); + + [Flags] + internal enum DrawIconFlags : uint + { + DI_COMPAT = 0x0004, + DI_DEFAULTSIZE = 0x0008, + DI_IMAGE = 0x0002, + DI_MASK = 0x0001, + DI_NOMIRROR = 0x0010, + DI_NORMAL = DI_IMAGE | DI_MASK // 0x0003 + } + + // https://docs.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-endbufferedpaint + [DllImport("uxtheme.dll")] + internal static extern uint EndBufferedPaint(IntPtr hBufferedPaint, bool fUpdateTarget); + + [DllImport("user32.dll")] + internal static extern bool EndPaint(IntPtr hWnd, [In] ref PAINTSTRUCT lpPaint); + + // + + [StructLayout(LayoutKind.Sequential)] + internal struct PAINTSTRUCT + { + public IntPtr hdc; + public bool fErase; + public RECT rcPaint; + public bool fRestore; + public bool fIncUpdate; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + public byte[] rgbReserved; + } + + #endregion +} diff --git a/Morphic.Controls/TrayButton/Windows10/TrayButton.cs b/Morphic.Controls/TrayButton/Windows10/TrayButton.cs new file mode 100644 index 00000000..5258a3ff --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows10/TrayButton.cs @@ -0,0 +1,1616 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Morphic.Controls.TrayButton.Windows10; + +// TODO: resize the task button container back to where it started after we hide our tray button +// TODO: sometimes, Windows resizes the taskbar under us (in which case the task bar container runs underneath our button); we need to detect this and re-reposition gracefully +// TODO: add support for high contrast icons +// TODO: in some testing, we temporarily experienced a "spinning wheel" over our icon if the mouse cursor hovers over it (right after startup) + +internal class TrayButton : IDisposable +{ + private bool disposedValue; + + private System.Drawing.Icon? _icon = null; + private string? _text = null; + private bool _visible = false; + + private TrayButtonNativeWindow? _nativeWindow = null; + + //private bool _highContrastModeIsOn_Cached = false; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + public System.Drawing.Rectangle? PositionAndSize + { + get + { + return _nativeWindow?.PositionAndSize; + } + } + + internal TrayButton() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + this.DestroyManagedNativeWindow(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrayButton() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// The icon for the tray button + public System.Drawing.Icon? Icon + { + get + { + return _icon; + } + set + { + _icon = value; + + _nativeWindow?.SetIcon(_icon); + } + } + + /// Tooltip for the tray button. + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + + _nativeWindow?.SetText(_text); + } + } + + /// Show or hide the tray button. + public bool Visible + { + get + { + return _visible; + } + set + { + _visible = value; + + if (_visible == true) + { + if (_nativeWindow is null) + { + CreateNativeWindow(); + } + } + else //if (_visible == false) + { + if (_nativeWindow is not null) + { + this.DestroyManagedNativeWindow(); + } + } + } + } + + // NOTE: this throws an exception if it fails to create the native window + private void CreateNativeWindow() + { + // if the tray button window already exists; it cannot be created again + if (_nativeWindow is not null) + { + throw new InvalidOperationException(); + } + + // find the window handle of the Windows taskbar + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + // could not find taskbar + throw new Exception("Could not find taskbar"); + } + + /* TODO: consider cached the current DPI of the taskbar (to track, in case the taskbar DPI changes in the future); we currently calculate the icon size based on + * the height/width of the window, so this check may not be necessary */ + + //// cache the current high contrast on/off state (to track) + //_highContrastModeIsOn_Cached = IsHighContrastModeOn(); + + // create the native window + var nativeWindow = new TrayButtonNativeWindow(this); + + // initialize the native window; note that we have separated "initialize" into a separate function so that our constructor doesn't throw exceptions on failure + try + { + nativeWindow.Initialize(taskbarHandle); + } + catch (System.ComponentModel.Win32Exception/* ex*/) + { + // TODO: consider what exceptions we could get here, how to handle them and how to bubble them up to our caller, etc. + throw; + } + catch (InvalidOperationException) + { + throw; + } + + // set the icon for the native window + nativeWindow.SetIcon(_icon); + // set the (tooltip) text for the native window + nativeWindow.SetText(_text); + + // store the reference to our new native window + _nativeWindow = nativeWindow; + } + + private void DestroyManagedNativeWindow() + { + _nativeWindow?.Dispose(); + _nativeWindow = null; + } + + //private bool IsHighContrastModeOn() + //{ + // var highContrastIsOn = (Spi.Instance.GetHighContrast() & Spi.HighContrastOptions.HCF_HIGHCONTRASTON) != 0; + // return highContrastIsOn; + //} + + #region Tray Button (Native Window) + + private class TrayButtonNativeWindow : System.Windows.Forms.NativeWindow, IDisposable + { + private TrayButton _owner; + + private IntPtr _tooltipWindowHandle = IntPtr.Zero; + private IntPtr _iconHandle = IntPtr.Zero; + + private string? _tooltipText = null; + private bool _tooltipInfoAdded = false; + + // NOTE: this timer is used to reposition the tray button when the screen resolution changes (and it keeps watch at an accelerated pace, to make sure the taskbar has stopped moving around) + private System.Threading.Timer? _trayButtonPositionCheckupTimer; + private int _trayButtonPositionCheckupTimerCounter = 0; + + // NOTE: this timer is used to reposition the tray button when adjacent taskbar widgets (e.g. Windows 10 weather) change in size + private System.Threading.Timer? _trayButtonWidgetPositionCheckupTimer; + // + private TimeSpan _widgetPositionCheckupInterval = new TimeSpan(0, 0, 0, 1, 0); // NOTE: this is a failsafe mechanism; if our position changes more than this many times per minute, we will back off the widget reposition timer (to avoid a potential super-glitchy user experience, with a taskbar button that won't stop moving; this may also help avoid the unnecessary movement of adjacent widgets) + private Queue _widgetPositionChangeHistory = new(); + + [Flags] + private enum TrayButtonVisualStateFlags + { + None = 0, + Hover = 1, + LeftButtonPressed = 2, + RightButtonPressed = 4 + } + private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; + + private Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook? _mouseHook = null; + + private System.Drawing.Rectangle _trayButtonPositionAndSize; + public System.Drawing.Rectangle PositionAndSize + { + get + { + return _trayButtonPositionAndSize; + } + } + + internal TrayButtonNativeWindow(TrayButton owner) + { + _owner = owner; + } + + public void Initialize(IntPtr taskbarHandle) + { + const string nativeWindowClassName = "Morphic-TrayButton"; + + // register our custom native window class + var pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(this.WndProcCallback)); + var lpWndClass = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = LegacyWindowsApi.LoadCursor(IntPtr.Zero, (int)LegacyWindowsApi.Cursors.IDC_ARROW) + }; + + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClass); + if (registerClassResult == 0) + { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + } + + var windowParams = new System.Windows.Forms.CreateParams(); + windowParams.ExStyle = (int)LegacyWindowsApi.WindowStylesEx.WS_EX_TOOLWINDOW; + /* NOTE: as we want to be able to ensure that we're referencing the exact class we just registered, we pass the RegisterClassEx results into the + * CreateWindow function (and we encode that result as a ushort here in a proprietary way) */ + windowParams.ClassName = registerClassResult.ToString(); // nativeWindowClassName; + //windowParams.Caption = nativeWindowClassName; + windowParams.Style = (int)(LegacyWindowsApi.WindowStyles.WS_VISIBLE | LegacyWindowsApi.WindowStyles.WS_CHILD | LegacyWindowsApi.WindowStyles.WS_CLIPSIBLINGS | LegacyWindowsApi.WindowStyles.WS_TABSTOP); + windowParams.X = 0; + windowParams.Y = 0; + windowParams.Width = 32; + windowParams.Height = 40; + windowParams.Parent = taskbarHandle; + // + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException, or System.ComponentModel.Win32Exception + this.CreateHandle(windowParams); + + // create the tooltip window (although we won't provide it with any actual text until/unless the text is set + this.CreateTooltipWindow(); + + // subscribe to display settings changes (so that we know when the screen resolution changes, so that we can reposition our button) + Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged; + + // if the user is using Windows 11, create a mouse message hook (so we can capture the mousemove and click events over our taskbar icon) + if (Morphic.WindowsNative.OsVersion.OsVersion.IsWindows11OrLater() == true) + { + _mouseHook = new Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook(); + _mouseHook.WndProcEvent += _mouseHook_WndProcEvent; + } + + // position the tray button in its initial position + // NOTE: the button has no icon at this point; if we want to move this logic to the Icon set routine, + // that's reasonable, but we'd need to think through any side-effects (and we'd need to do this here anyway + // if an icon had already been set prior to .Initialize being called) + //if (_iconHandle != IntPtr.Zero) + //{ + this.PositionTrayButton(); + //} + + // NOTE: due to the weather, news and other taskbar widgets introduced in late versions of Windows 10, we need to re-validate and re-position the taskbar icon when adjacent widgets overlay its position + _trayButtonWidgetPositionCheckupTimer = new System.Threading.Timer(TrayButtonWidgetPositionCheckup, null, _widgetPositionCheckupInterval, _widgetPositionCheckupInterval); + } + + // NOTE: this function is somewhat redundant and is provided to support Windows 11; we should refactor all of this code to handle window messages centrally + private void _mouseHook_WndProcEvent(object? sender, Morphic.Controls.TrayButton.Windows10.WindowsNative.MouseWindowMessageHook.WndProcEventArgs e) + { + // TODO: we should ensure that calls are queued and then called from a sequential thread (ideally a UI dispatch thread) + switch ((LegacyWindowsApi.WindowMessage)e.Message) + { + case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + { + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, e.X, e.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); + } + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because + // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-enable the pressed state if/where appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0)) + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + } + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0)) + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + } + // + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + { + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, e.X, e.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); + } + break; + } + } + + internal void SetText(string? text) + { + _tooltipText = text; + this.UpdateTooltipTextAndTracking(); + } + + private void CreateTooltipWindow() + { + if (_tooltipWindowHandle != IntPtr.Zero) + { + // tooltip window already exists + return; + } + + _tooltipWindowHandle = LegacyWindowsApi.CreateWindowEx( + 0 /* no styles */, + PInvokeExtensions.TOOLTIPS_CLASS, + null, + LegacyWindowsApi.WindowStyles.WS_POPUP | (LegacyWindowsApi.WindowStyles)PInvokeExtensions.TTS_ALWAYSTIP, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (_tooltipWindowHandle == IntPtr.Zero) + { + Debug.Assert(false, "Could not create tooltip window"); + } + + this.UpdateTooltipTextAndTracking(); + } + + private void DestroyTooltipWindow() + { + // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. + _tooltipText = null; + this.UpdateTooltipTextAndTracking(); + + LegacyWindowsApi.DestroyWindow(_tooltipWindowHandle); + _tooltipWindowHandle = IntPtr.Zero; + } + + private void UpdateTooltipTextAndTracking() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + // tooltip window does not exist; failed; abort + Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); + return; + } + + Windows.Win32.Foundation.RECT trayButtonClientRect; + var getClientRectSuccess = Windows.Win32.PInvoke.GetClientRect((Windows.Win32.Foundation.HWND)this.Handle, out trayButtonClientRect); + if (getClientRectSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip"); + return; + } + + var toolinfo = new PInvokeExtensions.TOOLINFO(); + toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); + toolinfo.hwnd = this.Handle; + toolinfo.uFlags = PInvokeExtensions.TTF_SUBCLASS; + toolinfo.lpszText = _tooltipText; + toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) + toolinfo.rect = trayButtonClientRect; + // + var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); + try + { + Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); + if (toolinfo.lpszText is not null) + { + if (_tooltipInfoAdded == false) + { + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_ADDTOOL, 0, pointerToToolinfo); + _tooltipInfoAdded = true; + } + else + { + // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_DELTOOL, 0, pointerToToolinfo); + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_ADDTOOL, 0, pointerToToolinfo); + } + } + else + { + // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed + _ = LegacyWindowsApi.SendMessage(_tooltipWindowHandle, (int)PInvokeExtensions.TTM_DELTOOL, 0, pointerToToolinfo); + _tooltipInfoAdded = false; + } + } + finally + { + Marshal.FreeHGlobal(pointerToToolinfo); + } + } + + // NOTE: intial creation events are captured by this callback, but afterwards window messages are captured by WndProc instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((LegacyWindowsApi.WindowMessage)msg) + { + case LegacyWindowsApi.WindowMessage.WM_CREATE: + if (LegacyWindowsApi.BufferedPaintInit() != LegacyWindowsApi.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process + } + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return LegacyWindowsApi.DefWindowProc(hWnd, msg, wParam, lParam); + } + + // NOTE: the built-in CreateHandle function couldn't handle our custom class, so we have overridden CreateHandle and are calling CreateWindowEx ourselves + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a (UInt16) number, convert that value to an IntPtr; otherwise capture a pointer to the string + IntPtr classNameAsIntPtr; + bool mustReleaseClassNameAsIntPtr = false; + // + ushort classNameAsUInt16 = 0; + if (ushort.TryParse(cp.ClassName, out classNameAsUInt16) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUInt16; + mustReleaseClassNameAsIntPtr = false; + } + else + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + mustReleaseClassNameAsIntPtr = true; + } + + // TODO: in some circumstances, it is possible that we are unable to create our window; consider creating a retry mechanism (dealing with async) or notify our caller + try + { + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + + // NOTE: in our testing, handle was sometimes IntPtr.Zero here (in which case the tray icon button's window will not exist) + if (handle == IntPtr.Zero) + { + Debug.Assert(false, "Could not create tray button window handle"); + } + + this.AssignHandle(handle); + } + finally + { + if (mustReleaseClassNameAsIntPtr == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + public void Dispose() + { + // TODO: if we are the topmost/leftmost next-to-tray-icon button, we should expand the task button container so it takes up our now-unoccupied space + + if (_mouseHook is not null) + { + _mouseHook.Dispose(); + } + + _trayButtonWidgetPositionCheckupTimer?.Dispose(); + _trayButtonWidgetPositionCheckupTimer = null; + + Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged; + + this.DestroyTooltipWindow(); + this.DestroyHandle(); + } + + protected override void WndProc(ref System.Windows.Forms.Message m) + { + var uMsg = (uint)m.Msg; + + IntPtr? result = null; + + switch ((LegacyWindowsApi.WindowMessage)uMsg) + { + case LegacyWindowsApi.WindowMessage.WM_DESTROY: + /* TODO: trace to see if WM_DESTROY is actually called here; if not, then we should place the uninit in dispose instead; we might also consider + * not using BufferedPaintInit/UnInit at all (although that _might_ slow down our buffered painting execution a tiny bit) */ + LegacyWindowsApi.BufferedPaintUnInit(); + break; + case LegacyWindowsApi.WindowMessage.WM_DISPLAYCHANGE: + // screen resolution has changed: reposition the tray button + // NOTE: m.wParam contains bit depth + // NOTE: m.lParam contains the resolutions of the screen (horizontal resolution in low-order word; vertical resolution in high-order word) + this.PositionTrayButton(); + break; + case LegacyWindowsApi.WindowMessage.WM_ERASEBKGND: + // we will handle erasing the background, so return a non-zero value here + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + { + var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (hitPoint is null) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + break; + } + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); + } + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEACTIVATE: + // do not activate our window (and discard this message) + result = new IntPtr(LegacyWindowsApi.MA_NOACTIVATEANDEAT); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSELEAVE: + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we remove the "pressed" focus as well as the "hover" focus because + // we aren't able to track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-enable the pressed state if/where appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_LBUTTON) != 0)) + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + } + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & LegacyWindowsApi.MK_RBUTTON) != 0)) + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + } + // + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_NCHITTEST: + var hitTestX = (short)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var hitTestY = (short)((m.LParam.ToInt64() >> 16) & 0xFFFF); + // + LegacyWindowsApi.RECT trayButtonRectInScreenCoordinates; + if (LegacyWindowsApi.GetWindowRect(this.Handle, out trayButtonRectInScreenCoordinates) == false) + { + // fail; abort + Debug.Assert(false, "Could not get rect of tray button in screen coordinates"); + return; + } + // + if ((hitTestX >= trayButtonRectInScreenCoordinates.Left) && (hitTestX < trayButtonRectInScreenCoordinates.Right) && + (hitTestY >= trayButtonRectInScreenCoordinates.Top) && (hitTestY < trayButtonRectInScreenCoordinates.Bottom)) + { + // inside client area + result = new IntPtr(1); // HTCLIENT + } + else + { + // nowhere + // TODO: determine if there is another response we should be returning instead; the documentation is not clear in this regard + result = new IntPtr(0); // HTNOWHERE + } + break; + case LegacyWindowsApi.WindowMessage.WM_NCPAINT: + // no non-client (frame) area to paint + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_PAINT: + this.Paint(m.HWnd); + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + { + var hitPoint = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (hitPoint is null) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + break; + } + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.Value.X, hitPoint.Value.Y, 0); + _owner.MouseUp?.Invoke(_owner, mouseArgs); + } + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_SETCURSOR: + // wParam: window handle + // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event + var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); + switch ((LegacyWindowsApi.WindowMessage)mouseMsg) + { + case LegacyWindowsApi.WindowMessage.WM_LBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + this.RequestRedraw(); + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_LBUTTONUP: + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_MOUSEMOVE: + // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter") then do so now + if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) + { + // track mousehover (for tooltips) and mouseleave (to remove hover effect) + var eventTrack = new LegacyWindowsApi.TRACKMOUSEEVENT(LegacyWindowsApi.TMEFlags.TME_LEAVE, this.Handle, LegacyWindowsApi.HOVER_DEFAULT); + var trackMouseEventSuccess = LegacyWindowsApi.TrackMouseEvent(ref eventTrack); + if (trackMouseEventSuccess == false) + { + // failed + Debug.Assert(false, "Could not set up tracking of tray button window area"); + return; + } + + _visualState |= TrayButtonVisualStateFlags.Hover; + + this.RequestRedraw(); + } + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONDOWN: + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + this.RequestRedraw(); + result = new IntPtr(1); + break; + case LegacyWindowsApi.WindowMessage.WM_RBUTTONUP: + result = new IntPtr(1); + break; + default: + //Debug.WriteLine("UNHANDLED SETCURSOR Mouse Message: " + mouseMsg.ToString()); + break; + } + break; + case LegacyWindowsApi.WindowMessage.WM_SIZE: + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGED: + result = new IntPtr(0); + break; + case LegacyWindowsApi.WindowMessage.WM_WINDOWPOSCHANGING: + // in this implementation, we don't do anything with this message; nothing to do here + result = new IntPtr(0); + break; + default: + // unhandled message; this will be passed onto DefWindowProc instead + break; + } + + if (result.HasValue == true) + { + m.Result = result.Value; + } + else + { + m.Result = LegacyWindowsApi.DefWindowProc(m.HWnd, (uint)m.Msg, m.WParam, m.LParam); + } + } + + private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) + { + // start a timer which will verify that the button is positioned properly (and will give up after a certain number of attempts) + var checkupInterval = new TimeSpan(0, 0, 0, 0, 250); + _trayButtonPositionCheckupTimerCounter = 40; // count down for 10 seconds (0.250 x 40) + _trayButtonPositionCheckupTimer = new System.Threading.Timer(TrayButtonPositionCheckup, null, checkupInterval, checkupInterval); + } + private void TrayButtonPositionCheckup(object? state) + { + if (_trayButtonPositionCheckupTimerCounter <= 0) + { + _trayButtonPositionCheckupTimer?.Dispose(); + _trayButtonPositionCheckupTimer = null; + return; + } + // + _trayButtonPositionCheckupTimerCounter = Math.Max(_trayButtonPositionCheckupTimerCounter - 1, 0); + + // check the current and desired positions of the notify tray icon + var calculateResult = this.CalculateCurrentAndTargetRectOfTrayButton(); + if (calculateResult is not null) + { + if (calculateResult.Value.changeToRect is not null) + { + this.PositionTrayButton(); + } + } + } + // + private void TrayButtonWidgetPositionCheckup(object? state) + { + const int NUM_CHANGE_HISTORY_ENTRIES_TO_AVERAGE = 10; + const int WIDGET_POSITION_INTERVAL_BACKOFF_MULTIPLIER = 2; + + // check the current and desired positions of the notify tray icon + var calculateResult = this.CalculateCurrentAndTargetRectOfTrayButton(); + if (calculateResult is not null) + { + if (calculateResult.Value.changeToRect is not null) + { + // record the repositioning event's timestamp + var currentRepositioningEventTimestamp = DateTimeOffset.UtcNow; + _widgetPositionChangeHistory.Enqueue(currentRepositioningEventTimestamp); + while (_widgetPositionChangeHistory.Count > NUM_CHANGE_HISTORY_ENTRIES_TO_AVERAGE) + { + _ = _widgetPositionChangeHistory.Dequeue(); + } + + // reposition the tray button + this.PositionTrayButton(); + + // determine if our repositioning is happening too frequently; if so, then increase its check interval (multiplied by WIDGET_POSITION_INTERVAL_BACKOFF_MULTIPLIER) + var oldestChange = _widgetPositionChangeHistory.Peek(); + var numberOfHistoryEvents = _widgetPositionChangeHistory.Count; + var totalDuration = currentRepositioningEventTimestamp.Subtract(oldestChange); + if (numberOfHistoryEvents == NUM_CHANGE_HISTORY_ENTRIES_TO_AVERAGE) + { + TimeSpan averageIntervalPerChange = totalDuration / numberOfHistoryEvents; + if (averageIntervalPerChange < _widgetPositionCheckupInterval * WIDGET_POSITION_INTERVAL_BACKOFF_MULTIPLIER) + { + _widgetPositionCheckupInterval *= WIDGET_POSITION_INTERVAL_BACKOFF_MULTIPLIER; + _trayButtonWidgetPositionCheckupTimer?.Change(_widgetPositionCheckupInterval, _widgetPositionCheckupInterval); + } + } + } + } + } + + private LegacyWindowsApi.POINT? ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) + { + var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); + var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); + // convert x and y to screen coordinates + var hitPoint = new PInvoke.POINT { x = x, y = y }; + + // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) + Marshal.SetLastPInvokeError(0); + // + // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); + if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != 0) + { + // failed; abort + Debug.Assert(false, "Could not map tray button hit point to screen coordinates"); + return null; + } + + var result = new LegacyWindowsApi.POINT(hitPoint.x, hitPoint.y); + return result; + } + private void Paint(IntPtr hWnd) + { + LegacyWindowsApi.PAINTSTRUCT ps = new LegacyWindowsApi.PAINTSTRUCT(); + IntPtr paintDc = LegacyWindowsApi.BeginPaint(hWnd, out ps); + try + { + IntPtr bufferedPaintDc; + // NOTE: ps.rcPaint was an empty rect in our intiail tests, so we are using a manually-created clientRect (from GetClientRect) here instead + var paintBufferHandle = LegacyWindowsApi.BeginBufferedPaint(ps.hdc, ref ps.rcPaint, LegacyWindowsApi.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, IntPtr.Zero, out bufferedPaintDc); + try + { + if (ps.rcPaint == LegacyWindowsApi.RECT.Empty) + { + // no rectangle; nothing to do + return; + } + + // clear our buffer background (to ARGB(0,0,0,0)) + var bufferedPaintClearSuccess = LegacyWindowsApi.BufferedPaintClear(paintBufferHandle, ref ps.rcPaint); + if (bufferedPaintClearSuccess != LegacyWindowsApi.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not clear tray button's background"); + return; + } + + // if the user has pressed (mousedown) on our tray button or is hovering over it, highlight the tray button now + Double highlightOpacity = 0.0; + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || + ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) + { + highlightOpacity = 0.25; + } + else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) + { + highlightOpacity = 0.1; + } + // + if (highlightOpacity > 0.0) + { + this.DrawHighlightBackground(bufferedPaintDc, ps.rcPaint, System.Drawing.Color.White, highlightOpacity); + } + + // calculate the size and position of our icon + int iconWidthAndHeight = this.CalculateWidthAndHeightForIcon(ps.rcPaint); + // + var xLeft = ((ps.rcPaint.Right - ps.rcPaint.Left) - iconWidthAndHeight) / 2; + var yTop = ((ps.rcPaint.Bottom - ps.rcPaint.Top) - iconWidthAndHeight) / 2; + + if (_iconHandle != IntPtr.Zero && iconWidthAndHeight > 0) + { + var drawIconSuccess = LegacyWindowsApi.DrawIconEx(bufferedPaintDc, xLeft, yTop, _iconHandle, iconWidthAndHeight, iconWidthAndHeight, 0 /* not animated */, IntPtr.Zero /* no triple-buffering */, LegacyWindowsApi.DrawIconFlags.DI_NORMAL | LegacyWindowsApi.DrawIconFlags.DI_NOMIRROR); + if (drawIconSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not draw tray button's icon"); + return; + } + } + } + finally + { + LegacyWindowsApi.EndBufferedPaint(paintBufferHandle, true); + } + } + finally + { + LegacyWindowsApi.EndPaint(hWnd, ref ps); + } + } + + private int CalculateWidthAndHeightForIcon(LegacyWindowsApi.RECT rect) + { + int result; + // NOTE: we currently measure the size of our icon by measuring the size of the rectangle + // NOTE: we use the larger of the two dimensions (height vs width) to determine our icon size; we may reconsider this in the future if we support non-square icons + int largerDimensionLenth; + if (rect.Bottom - rect.Top > rect.Right - rect.Left) + { + largerDimensionLenth = rect.Bottom - rect.Top; + } + else + { + largerDimensionLenth = rect.Right - rect.Left; + } + // + if (largerDimensionLenth >= 48) + { + result = 32; + } + else if (largerDimensionLenth >= 36) + { + result = 24; + } + else if (largerDimensionLenth >= 30) + { + result = 20; + } + else if (largerDimensionLenth >= 24) + { + result = 16; + } + else + { + result = 0; + } + + return result; + } + + private void DrawHighlightBackground(IntPtr hdc, LegacyWindowsApi.RECT rect, System.Drawing.Color color, Double opacity) + { + // GDI doesn't have a concept of semi-transparent pixels - the only function that honours them is AlphaBlend. + // Create a bitmap containing a single pixel - and then use AlphaBlend to stretch it to the size of the rect. + + // set up the 1x1 pixel bitmap's configuration + var pixelBitmapInfo = new LegacyWindowsApi.BITMAPINFO(); + pixelBitmapInfo.bmiHeader = new LegacyWindowsApi.BITMAPINFOHEADER() + { + biWidth = 1, + biHeight = 1, + biPlanes = 1, // must be 1 + biBitCount = 32, // maximum of 2^32 colors + biCompression = LegacyWindowsApi.BitmapCompressionType.BI_RGB, + biSizeImage = 0, + biClrUsed = 0, + biClrImportant = 0 + }; + pixelBitmapInfo.bmiHeader.biSize = (uint)Marshal.SizeOf(pixelBitmapInfo.bmiHeader); + pixelBitmapInfo.bmiColors = new LegacyWindowsApi.RGBQUAD[1]; + + // calculate the pixel color as a uint32 (in AARRGGBB order) + uint pixelColor = ( + (((uint)color.A) << 24) | // NOTE: we ignore the alpha value in our call to AlphaBlend + (((uint)color.R) << 16) | + (((uint)color.G) << 8) | + (((uint)color.B) << 0)); + + // create the memory device context for the pixel + var pixelDc = LegacyWindowsApi.CreateCompatibleDC(hdc); + if (pixelDc == IntPtr.Zero) + { + // failed; abort + Debug.Assert(false, "Could not create device context for highlight pixel."); + return; + } + try + { + IntPtr pixelDibBitValues; + var pixelDibHandle = LegacyWindowsApi.CreateDIBSection(pixelDc, ref pixelBitmapInfo, LegacyWindowsApi.DIB_RGB_COLORS, out pixelDibBitValues, IntPtr.Zero, 0); + if (pixelDibHandle == IntPtr.Zero) + { + // failed; abort + Debug.Assert(false, "Could not create DIB for highlight pixel."); + return; + } + // + try + { + var selectedBitmapHandle = LegacyWindowsApi.SelectObject(pixelDc, pixelDibHandle); + if (selectedBitmapHandle == IntPtr.Zero) + { + // failed; abort + Debug.Assert(false, "Could not select object into the pixel device context."); + return; + } + try + { + // write over the single pixel's value (with the passed-in pixel) + Marshal.WriteIntPtr(pixelDibBitValues, new IntPtr(pixelColor)); + + // draw the highlight (stretching the pixel to the full rectangle size) + LegacyWindowsApi.BLENDFUNCTION blendFunction = new LegacyWindowsApi.BLENDFUNCTION() + { + BlendOp = (byte)LegacyWindowsApi.AC_SRC_OVER, + BlendFlags = 0, // must be zero + SourceConstantAlpha = (byte)(opacity * 255), // the requested opacity level + AlphaFormat = 0 + }; + var RESULT_TO_USE = LegacyWindowsApi.AlphaBlend(hdc, rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top, pixelDc, 0, 0, 1, 1, blendFunction); + } + finally + { + _ = LegacyWindowsApi.SelectObject(pixelDc, selectedBitmapHandle); + } + } + finally + { + _ = LegacyWindowsApi.DeleteObject(pixelDibHandle); + } + } + finally + { + _ = LegacyWindowsApi.DeleteDC(pixelDc); + } + } + + public void SetIcon(System.Drawing.Icon? icon) + { + if (icon is not null) + { + _iconHandle = icon.Handle; + } + else + { + _iconHandle = IntPtr.Zero; + } + + // TODO: if we support non-square icons, then reposition the tray button based on the new dimensions of the icon (in case it's wider/narrower) + //this.PositionTrayButton(); + + // trigger a redraw + this.RequestRedraw(); + } + + private void PositionTrayButton() + { + var trayButtonRects = CalculateCurrentAndTargetRectOfTrayButton(); + if (trayButtonRects is null) + { + // fail; abort + Debug.Assert(false, "Could not calculate current and/or new rects for tray button"); + return; + } + // + var currentRect = trayButtonRects.Value.currentRect; + var changeToRect = trayButtonRects.Value.changeToRect; + var taskbarOrientation = trayButtonRects.Value.orientation; + + if (_mouseHook is not null) + { + // update our tracking region to track the new position (unless we haven't moved, in which case continue to track our current position) + if (changeToRect is not null) + { + _mouseHook.UpdateTrackingRegion(changeToRect.Value.ToPInvokeRect()); + } + else if (currentRect is not null) + { + _mouseHook.UpdateTrackingRegion(currentRect.Value.ToPInvokeRect()); + } + else + { + Debug.Assert(false, "Could not determine current RECT of tray button"); + } + } + + // if changeToRect is more leftmost/topmost than the task button container's right side, then shrink the task button container appropriately + LegacyWindowsApi.RECT? newTaskButtonContainerRect = null; + if (changeToRect is not null) + { + var taskbarTripletHandles = this.GetTaskbarTripletHandles(); + var taskbarTripletRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); + if (taskbarTripletRects is null) + { + // failed; abort + Debug.Assert(false, "could not get rects of taskbar or its important children"); + return; + } + var taskButtonContainerRect = taskbarTripletRects.Value.TaskButtonContainerRect; + + if ((taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) && (taskButtonContainerRect.Right > changeToRect.Value.Left)) + { + newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( + taskButtonContainerRect.Left, + taskButtonContainerRect.Top, + Math.Max(taskButtonContainerRect.Right - taskButtonContainerRect.Left - (taskButtonContainerRect.Right - changeToRect.Value.Left), 0), + taskButtonContainerRect.Bottom - taskButtonContainerRect.Top + )); + } + else if ((taskbarOrientation == System.Windows.Forms.Orientation.Vertical) && taskButtonContainerRect.Bottom > changeToRect.Value.Top) + { + newTaskButtonContainerRect = new LegacyWindowsApi.RECT(new System.Windows.Rect( + taskButtonContainerRect.Left, + taskButtonContainerRect.Top, + taskButtonContainerRect.Right - taskButtonContainerRect.Left, + taskButtonContainerRect.Bottom - taskButtonContainerRect.Top - Math.Max(taskButtonContainerRect.Bottom - changeToRect.Value.Top, 0) + )); + } + } + // + if (newTaskButtonContainerRect is not null) + { + var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); + + // shrink the task button container + // NOTE: this is a blocking call, waiting until the task button container is resized; we do this intentionally so that we see its updated size synchronously + var repositionTaskButtonContainerSuccess = LegacyWindowsApi.SetWindowPos( + taskButtonContainerHandle, + IntPtr.Zero, + newTaskButtonContainerRect.Value.Left, + newTaskButtonContainerRect.Value.Top, + newTaskButtonContainerRect.Value.Right - newTaskButtonContainerRect.Value.Left, + newTaskButtonContainerRect.Value.Bottom - newTaskButtonContainerRect.Value.Top, + LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_NOMOVE /* retain the current x and y position, out of an abundance of caution */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_NOZORDER /* retain the current Z order (ignoring the hWndInsertAfter parameter) */ + ); + + if (repositionTaskButtonContainerSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not resize taskbar's task button container"); + return; + } + + // capture our control's native window's new position and size + // NOTE: since we suppressed repositioning of the taskbar container above (i.e. just resizing it), we are only capturing the updated size here (out of an abundance of caution) + _trayButtonPositionAndSize.Width = newTaskButtonContainerRect!.Value.Right - newTaskButtonContainerRect!.Value.Left; + _trayButtonPositionAndSize.Height = newTaskButtonContainerRect!.Value.Bottom - newTaskButtonContainerRect!.Value.Top; + } + + // if our button needs to move (either because we don't know the old RECT or because the new RECT is different), do so now + if (changeToRect is not null) + { + if (currentRect.HasValue == false || (currentRect.Value != changeToRect.Value)) + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + + // convert our tray button's position from desktop coordinates to "child" coordinates within the taskbar + PInvoke.RECT childRect = new() { left = changeToRect.Value.Left, top = changeToRect.Value.Top, right = changeToRect.Value.Right, bottom = changeToRect.Value.Bottom }; + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(IntPtr.Zero /* use screen coordinates */, taskbarHandle, ref childRect, 2 /* 2 indicates that lpPoints is a RECT */); + if (mapWindowPointsResult == 0 && Marshal.GetLastWin32Error() != LegacyWindowsApi.ERROR_SUCCESS) + { + // failed; abort + Debug.Assert(false, "Could not map tray button RECT points to taskbar window handle"); + return; + } + + var repositionTrayButtonSuccess = LegacyWindowsApi.SetWindowPos( + this.Handle, + LegacyWindowsApi.HWND_TOP, + childRect.left, + childRect.top, + childRect.right - childRect.left, + childRect.bottom - childRect.top, + LegacyWindowsApi.SetWindowPosFlags.SWP_NOACTIVATE /* do not activate the window */ | + LegacyWindowsApi.SetWindowPosFlags.SWP_SHOWWINDOW /* display the tray button */ + ); + + if (repositionTrayButtonSuccess == false) + { + // failed; abort + Debug.Assert(false, "Could not reposition and/or resize tray button"); + return; + } + + // capture our control's native window's new position and size + _trayButtonPositionAndSize = new(childRect.left, childRect.top, childRect.right - childRect.left, childRect.bottom - childRect.top); + } + + // as we have moved/resized, request a repaint + this.RequestRedraw(); + + // if we have tooltip text, update its tracking rectangle + if (_tooltipText is not null) + { + UpdateTooltipTextAndTracking(); + } + } + } + + private (IntPtr TaskbarHandle, IntPtr TaskButtonContainerHandle, IntPtr NotifyTrayHandle) GetTaskbarTripletHandles() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + var taskButtonContainerHandle = TrayButtonNativeWindow.FindWindowsTaskbarTaskButtonContainerHandle(); + var notifyTrayHandle = TrayButtonNativeWindow.FindWindowsTaskbarNotificationTrayHandle(); + + return (taskbarHandle, taskButtonContainerHandle, notifyTrayHandle); + } + + private (LegacyWindowsApi.RECT TaskbarRect, LegacyWindowsApi.RECT TaskButtonContainerRect, LegacyWindowsApi.RECT NotifyTrayRect)? GetTaskbarTripletRects(IntPtr taskbarHandle, IntPtr taskButtonContainerHandle, IntPtr notifyTrayHandle) + { + // find the taskbar and its rect + LegacyWindowsApi.RECT taskbarRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskbarHandle, out taskbarRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar."); + return null; + } + + // find the window handles and rects of the task button container and the notify tray (which are children inside of the taskbar) + // + LegacyWindowsApi.RECT taskButtonContainerRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskButtonContainerHandle, out taskButtonContainerRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar's task button list container."); + return null; + } + // + LegacyWindowsApi.RECT notifyTrayRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(notifyTrayHandle, out notifyTrayRect) == false) + { + // failed; abort + Debug.Assert(false, "Could not obtain window handle to taskbar's notify tray."); + return null; + } + + return (taskbarRect, taskButtonContainerRect, notifyTrayRect); + } + + private (LegacyWindowsApi.RECT availableAreaRect, List childRects) CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(IntPtr taskbarHandle, System.Windows.Forms.Orientation taskbarOrientation, bool isRightToLeft, LegacyWindowsApi.RECT taskbarRect, LegacyWindowsApi.RECT taskButtonContainerRect, LegacyWindowsApi.RECT notifyTrayRect) + { + // calculate the total "free area" rectangle (the area between the task button container and the notify tray where we want to place our tray button) + LegacyWindowsApi.RECT freeAreaAvailableRect; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + if (isRightToLeft == false) + { + freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskButtonContainerRect.Right, taskbarRect.Top, Math.Max(notifyTrayRect.Left - taskButtonContainerRect.Right, 0), Math.Max(taskbarRect.Bottom - taskbarRect.Top, 0))); + } + else + { + freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(notifyTrayRect.Right, taskbarRect.Top, Math.Max(taskButtonContainerRect.Left - notifyTrayRect.Right, 0), Math.Max(taskbarRect.Bottom - taskbarRect.Top, 0))); + } + } + else + { + freeAreaAvailableRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(taskbarRect.Left, taskButtonContainerRect.Bottom, Math.Max(taskbarRect.Right - taskbarRect.Left, 0), Math.Max(notifyTrayRect.Top - taskButtonContainerRect.Bottom, 0))); + } + + // capture a list of all child windows within the taskbar; we'll use this list to enumerate the rects of all the taskbar's children + var taskbarChildHandles = TrayButtonNativeWindow.EnumerateChildWindows(taskbarHandle); + // + // find the rects of all windows within the taskbar; we need this information so that we do not overlap any other accessory windows which are trying to sit in the same area as us + var taskbarChildHandlesWithRects = new Dictionary(); + foreach (var taskbarChildHandle in taskbarChildHandles) + { + LegacyWindowsApi.RECT taskbarChildRect = new LegacyWindowsApi.RECT(); + if (LegacyWindowsApi.GetWindowRect(taskbarChildHandle, out taskbarChildRect) == true) + { + taskbarChildHandlesWithRects.Add(taskbarChildHandle, taskbarChildRect); + } + else + { + Debug.Assert(false, "Could not capture RECTs of all taskbar child windows"); + } + } + + // remove any child rects which are contained inside the task button container (so that we eliminate any subchildren from our calculations) + foreach (var taskbarChildHandle in taskbarChildHandles) + { + if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + { + var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; + if (taskbarChildRect.IsInside(taskButtonContainerRect)) + { + taskbarChildHandlesWithRects.Remove(taskbarChildHandle); + } + } + } + + // remove our own (tray button) window handle from the list (so that we don't see our current screen rect as "taken" in the list of occupied RECTs) + taskbarChildHandlesWithRects.Remove(this.Handle); + + // create a list of children which are located between the task button container and the notify tray (i.e. windows which are occupying the same region we want to + // occupy...so we can try to avoid overlapping) + List freeAreaChildRects = new List(); + foreach (var taskbarChildHandle in taskbarChildHandles) + { + if (taskbarChildHandlesWithRects.ContainsKey(taskbarChildHandle) == true) + { + var taskbarChildRect = taskbarChildHandlesWithRects[taskbarChildHandle]; + if ((taskbarChildRect.IsInside(freeAreaAvailableRect) == true) && + (taskbarChildRect.HasNonZeroWidthOrHeight() == false)) + { + freeAreaChildRects.Add(taskbarChildRect); + } + } + } + + return (freeAreaAvailableRect, freeAreaChildRects); + } + + // NOTE: this function returns a newPosition IF the tray button should be moved + private (LegacyWindowsApi.RECT? currentRect, LegacyWindowsApi.RECT? changeToRect, System.Windows.Forms.Orientation orientation)? CalculateCurrentAndTargetRectOfTrayButton() + { + // NOTE: there are scenarios we must deal with where there may be multiple potential "taskbar button" icons to the left of the notification tray; in those scenarios, we must: + // 1. Position ourself to the left of the other icon-button(s) (or in an empty space in between them) + // 2. Reposition our icon when the other icon-button(s) are removed from the taskbar (e.g. when their host applications close them) + // 3. If we detect that we and another application are writing on top of each other (or repositioning the taskbar button container on top of our icon), then we must fail + // gracefully and let our host application know so it can warn the user, place the icon in the notification tray instead, etc. + + // To position the tray button, we need to find three windows: + // 1. the taskbar itself + // 2. the section of the taskbar which holds the taskbar buttons (i.e. to the right of the start button and find/cortana/taskview buttons, but to the left of the notification tray) */ + // 3. the notification tray + // + // We will then resize the section of the taskbar that holds the taskbar buttons so that we can place our tray button to its right (i.e. to the left of the notification tray). + + var taskbarTripletHandles = this.GetTaskbarTripletHandles(); + var taskbarHandle = taskbarTripletHandles.TaskbarHandle; + + var taskbarRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); + if (taskbarRects is null) + { + return null; + } + var taskbarRect = taskbarRects.Value.TaskbarRect; + var taskButtonContainerRect = taskbarRects.Value.TaskButtonContainerRect; + var notifyTrayRect = taskbarRects.Value.NotifyTrayRect; + + // determine the taskbar's orientation + System.Windows.Forms.Orientation taskbarOrientation; + if ((taskbarRect.Right - taskbarRect.Left) > (taskbarRect.Bottom - taskbarRect.Top)) + { + taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; + } + else + { + taskbarOrientation = System.Windows.Forms.Orientation.Vertical; + } + + // if the taskbar is horizontal, determine if it's LeftToRight (standard) or RightToLeft (for Arabic, Hebrew, etc.) + bool isRightToLeft = false; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + var centerXOfTaskbar = taskbarRect.Left + ((taskbarRect.Right - taskbarRect.Left) / 2); + if (notifyTrayRect.Right < centerXOfTaskbar) + { + isRightToLeft = true; + } + } + + // calculate all of the free rects between the task button container and notify tray + var calculateEmptyRectsResult = this.CalculateEmptyRectsBetweenTaskButtonContainerAndNotifyTray(taskbarHandle, taskbarOrientation, isRightToLeft, taskbarRect, taskButtonContainerRect, notifyTrayRect); + var freeAreaChildRects = calculateEmptyRectsResult.childRects; + var freeAreaAvailableRect = calculateEmptyRectsResult.availableAreaRect; + + /* determine the rect for our tray button; based on our current positioning strategy, this will either be its existing position or the leftmost/topmost "next to tray" position. + * If we are determining the leftmost/topmost "next to tray" position, we will find the available space between the task button container and the notification tray (or any + * already-present controls that are already left/top of the notification tray); if there is not enough free space available in that area then we will shrink the task button + * container to make room. */ + // + /* NOTE: there are some deficiencies to our current positioning strategy. Of note... + * 1. In some circumstances, it might be possible that we are leaving "holes" of available space between the task button container and the notification tray; but if that + * happens, it might be something beyond our control (as other apps may have created that space). One concern is if we shrink our icon (in which case we should in theory + * shrink the space to our top/left) + * 2. If other apps draw their next-to-tray buttons after us and are not watching for conflicts then they could draw over us; a mitigation measure in that instance might be to + * use a timer to check that our tray button is not obscured and then remedy the situation; if we got into a "fight" over real estate that appeared to never terminate then + * we could destroy our icon and raise an event letting the application know it should choose an alternate strategy (such as a notification tray icon) instead. + * 3. If a more-rightmost/bottommost icon's application is closed while we are running, the taskbar could be resized to obscure us; we might need a timer (or we might need to + * capture the appropriate window message) to discover this scenario. + * In summary there is no standardized system (other than perhaps the "(dock) toolbar in taskbar" mechanism); if we find that we encounter problems in the field with our current + * strategy, we may want to consider rebuilding this functionality via the "toolbar in taskbar" mechanism. See HP Support Assistant for an example of another application + * which is doing what we are trying to do with the next-to-tray button strategy */ + + // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) + int trayButtonHeight; + int trayButtonWidth; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + trayButtonHeight = taskbarRect.Bottom - taskbarRect.Top; + trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); + } + else + { + trayButtonWidth = taskbarRect.Right - taskbarRect.Left; + trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); + } + + // get our current rect (in case we can just reuse the current position...and also to make sure it doesn't need to be resized) + LegacyWindowsApi.RECT currentRectAsNonNullable; + LegacyWindowsApi.RECT? currentRect = null; + LegacyWindowsApi.RECT? currentRectForResult = null; + if (LegacyWindowsApi.GetWindowRect(this.Handle, out currentRectAsNonNullable) == true) + { + currentRect = currentRectAsNonNullable; + currentRectForResult = currentRectAsNonNullable; + } + + // if the current position of our window isn't the right size for our icon, then set it to NULL so we don't try to reuse it. + if ((currentRect is not null) && + ((currentRect.Value.Right - currentRect.Value.Left != trayButtonWidth) || (currentRect.Value.Bottom - currentRect.Value.Top != trayButtonHeight))) + { + currentRect = null; + } + + // calculate the new rect for our tray button's window + LegacyWindowsApi.RECT? newRect = null; + + // if the space occupied by our already-existing rect is not overlapped by anyone else and is in the free area, keep using the same space + if ((currentRect is not null) && (currentRect.Value.Intersects(freeAreaAvailableRect) == true)) + { + // by default, assume that our currentRect is still available (i.e. not overlapped) + bool currentRectIsNotOverlapped = true; + + // make sure we do not overlap another control in the free area + foreach (var freeAreaChildRect in freeAreaChildRects) + { + if (currentRect.Value.Intersects(freeAreaChildRect) == true) + { + // overlap conflict + currentRectIsNotOverlapped = false; + break; + } + } + + if (currentRectIsNotOverlapped == true) + { + // set "newRect" (the variable for where we will now place our tray button) to the same position we were already at + newRect = currentRect; + } + } + + // if our current (already-used-by-us) rect was not available, choose the leftmost/topmost space available; note that "rightmost" is actually leftmost when the system is using an RTL orientation (e.g. Arabic, Hebrew) + if (newRect is null) + { + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + // horizontal taskbar: find the leftmost rect in the available space (which we'll then carve the "rightmost" section out of) + // OBSERVATION: leftmost is actually rightmost in RTL layouts (e.g. Arabic, Hebrew) + LegacyWindowsApi.RECT leftmostRect = freeAreaAvailableRect; + + foreach (var freeAreaChildRect in freeAreaChildRects) + { + if (isRightToLeft == false) + { + if (freeAreaChildRect.Left < leftmostRect.Right) + { + leftmostRect.Right = freeAreaChildRect.Left; + } + } + else + { + if (freeAreaChildRect.Right > leftmostRect.Left) + { + leftmostRect.Left = freeAreaChildRect.Right; + } + } + } + + // choose the rightmost space in the leftmostRect area (or leftmost for RTL layouts); expand our tray button towards the left (right for RTL) if/as necessary + if (isRightToLeft == false) + { + newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(leftmostRect.Right - trayButtonWidth, leftmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + } + else + { + newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(leftmostRect.Left, leftmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + } + } + else + { + // vertical taskbar: find the topmost rect in the available space (which we'll then carve the "bottommost" section out of) + LegacyWindowsApi.RECT topmostRect = freeAreaAvailableRect; + + foreach (var freeAreaChildRect in freeAreaChildRects) + { + if (freeAreaChildRect.Top < topmostRect.Bottom) + { + topmostRect.Bottom = freeAreaChildRect.Top; + } + } + + // choose the bottommost space in the topmostRect area; expand our tray button towards the top if/as necessary + newRect = new LegacyWindowsApi.RECT(new System.Windows.Rect(topmostRect.Right - trayButtonWidth, topmostRect.Bottom - trayButtonHeight, trayButtonWidth, trayButtonHeight)); + } + } + + LegacyWindowsApi.RECT? changeToRect = null; + if (newRect != currentRectForResult) + { + changeToRect = newRect; + } + + return (currentRectForResult, changeToRect, taskbarOrientation); + } + + private bool RequestRedraw() + { + return LegacyWindowsApi.RedrawWindow( + this.Handle, + IntPtr.Zero, + IntPtr.Zero, + LegacyWindowsApi.RedrawWindowFlags.RDW_ERASE | LegacyWindowsApi.RedrawWindowFlags.RDW_INVALIDATE | LegacyWindowsApi.RedrawWindowFlags.RDW_ALLCHILDREN + ); + } + + internal static List EnumerateChildWindows(IntPtr parentHwnd) + { + var result = new List(); + + // create an unmanaged pointer to our list (using a GC-managed handle) + GCHandle resultGCHandle = GCHandle.Alloc(result, GCHandleType.Normal); + // convert our GCHandle into an IntPtr (which we will unconvert back to a GCHandler in the EnumChildWindows callback) + IntPtr resultGCHandleAsIntPtr = GCHandle.ToIntPtr(resultGCHandle); + + try + { + var enumFunction = new LegacyWindowsApi.EnumWindowsProc(TrayButtonNativeWindow.EnumerateChildWindowsCallback); + LegacyWindowsApi.EnumChildWindows(parentHwnd, enumFunction, resultGCHandleAsIntPtr); + + } + finally + { + if (resultGCHandle.IsAllocated) + { + resultGCHandle.Free(); + } + } + + return result; + } + internal static bool EnumerateChildWindowsCallback(IntPtr hwnd, IntPtr lParam) + { + // convert lParam back into the result list object + var resultGCHandle = GCHandle.FromIntPtr(lParam); + List? result = resultGCHandle.Target as List; + + if (result is not null) + { + result.Add(hwnd); + } + else + { + Debug.Assert(false, "Could not enumerate child windows"); + } + + return true; + } + + internal static IntPtr FindWindowsTaskbarHandle() + { + return LegacyWindowsApi.FindWindow("Shell_TrayWnd", null); + } + + private static IntPtr FindWindowsTaskbarTaskButtonContainerHandle() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "ReBarWindow32", null); + } + + private static IntPtr FindWindowsTaskbarNotificationTrayHandle() + { + var taskbarHandle = TrayButtonNativeWindow.FindWindowsTaskbarHandle(); + if (taskbarHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + return LegacyWindowsApi.FindWindowEx(taskbarHandle, IntPtr.Zero, "TrayNotifyWnd", null); + } + + } + #endregion +} diff --git a/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs b/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs new file mode 100644 index 00000000..2698fc54 --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows10/WindowsNative/MouseWindowMessageHook.cs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Morphic.Controls.TrayButton.Windows10.WindowsNative; + +public class MouseWindowMessageHook : IDisposable +{ + PInvoke.User32.WindowsHookDelegate _filterFunction; + PInvoke.User32.SafeHookHandle _hookHandle; + private bool _isDisposed; + + PInvoke.RECT? _trackingRect = null; + + public struct WndProcEventArgs + { + public uint Message; + public int X; + public int Y; + } + public event EventHandler? WndProcEvent; + + public MouseWindowMessageHook() + { + // NOTE: we are using a low-level hook in this implementation, and we are monitoring mouse events globally (and then filtering by RECT below) + _filterFunction = new PInvoke.User32.WindowsHookDelegate(this.MessageFilterProc); + _hookHandle = PInvoke.User32.SetWindowsHookEx(PInvoke.User32.WindowsHookType.WH_MOUSE_LL, _filterFunction, IntPtr.Zero, 0 /* global hook */); + } + + public void UpdateTrackingRegion(PInvoke.RECT rect) + { + _trackingRect = rect; + } + + bool _lastMessageWasInTrackingRect = false; + // NOTE: ideally, we would create a queue of messages and then use Task.Run to run code which dequeued the latest messages sequentially + private int MessageFilterProc(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode < 0) + { + // per Microsoft's docs: if the code is less than zero, we must pass the message along with _no_ intermediate processing + // see: https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644986(v=vs.85) + + // call the next hook in the chain and return its result + return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); + } + + // NOTE: as this is a low-level hook, we must process the message in less than the LowLevelHooksTimeout value (in ms) specified at: + // HKEY_CURRENT_USER\Control Panel\Desktop + // [for this reason and others, we simply capture the events and add them to a thread-safe queue...and then dispatch them to the UI thread's event loop] + + switch (nCode) + { + case 0 /* HC_ACTION */: + // wParam and lParam contain information about a mouse message + { + // NOTE: wParam is one of: { WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP } + // NOTE: lParam is a MSLLHOOKSTRUCT structure instance; see: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msllhookstruct + + var mouseEventInfo = Marshal.PtrToStructure(lParam); + + var eventArgs = new WndProcEventArgs() + { + Message = (uint)wParam.ToInt64(), + X = mouseEventInfo.pt.x, + Y = mouseEventInfo.pt.y + }; + + if (_trackingRect is not null) + { + if ((mouseEventInfo.pt.x >= _trackingRect.Value.left) && + (mouseEventInfo.pt.x <= _trackingRect.Value.right) && + (mouseEventInfo.pt.y >= _trackingRect.Value.top) && + (mouseEventInfo.pt.y <= _trackingRect.Value.bottom)) + { + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + _lastMessageWasInTrackingRect = true; + } + else + { + if (_lastMessageWasInTrackingRect == true) + { + // send a WM_MOUSELEAVE event when the mouse leaves the tracking rect + eventArgs.Message = 0x02A3 /* WM_MOUSELEAVE */; + // + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + } + + _lastMessageWasInTrackingRect = false; + } + } + else + { + // there is no tracking RECT, so track globally + // + // NOTE: this may not be guaranteed to execute in sequence + Task.Run(() => { WndProcEvent?.Invoke(this, eventArgs); }); + _lastMessageWasInTrackingRect = true; + } + } + // NOTE: we are not "processing" the message, so we will always fall-through and let the next hook in the chain process the message + break; + default: + // unsupported code + break; + } + + // call the next hook in the chain and return its result + return PInvoke.User32.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); + } + + #region IDisposable + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // dispose any managed objects here + } + + // free unmanaged resources + + // NOTE: this function will return false if it fails + // NOTE: in theory the system should clean up after this hook handle automatically (so we could probably comment out the following two lines of code) + _ = LegacyWindowsApi.UnhookWindowsHookEx(_hookHandle.DangerousGetHandle()); + _hookHandle.SetHandleAsInvalid(); + + // set any large fields to null + + _isDisposed = true; + } + } + + ~MouseWindowMessageHook() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable +} diff --git a/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs b/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs new file mode 100644 index 00000000..b33364bb --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows11/ArgbImageNativeWindow.cs @@ -0,0 +1,649 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Morphic.Controls.TrayButton.Windows11; + +internal class ArgbImageNativeWindow : System.Windows.Forms.NativeWindow, IDisposable +{ + private bool disposedValue; + + // NOTE: SourceBitmap is the original bitmap passed into our object instance + private System.Drawing.Bitmap? _sourceBitmap = null; + // + // NOTE: SizedBitmap is the resized bitmap which we paint to our window + private System.Drawing.Bitmap? _sizedBitmap = null; + + private bool _visible; + + private static ushort? s_morphicArgbImageClassInfoExAtom = null; + + private ArgbImageNativeWindow() + { + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + // [none] + } + + // free unmanaged resources (unmanaged objects) and override finalizer + this.DestroyHandle(); + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // NOTE: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~ArgbImageNativeWindow() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public static MorphicResult CreateNew(IntPtr parentHWnd, int x, int y, int width, int height) + { + var result = new ArgbImageNativeWindow(); + + /* register a custom native window class for our ARGB Image window (or refer to the already-registered class, if we captured it earlier in the application's execution) */ + const string nativeWindowClassName = "Morphic-ArgbImage"; + // + if (s_morphicArgbImageClassInfoExAtom is null) + { + // register our control's custom native window class + nint pointerToWndProcCallback; + try + { + pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(result.WndProcCallback)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + // + var hCursor = Windows.Win32.PInvoke.LoadCursor(Windows.Win32.Foundation.HINSTANCE.Null, Windows.Win32.PInvoke.IDC_ARROW); + if (hCursor.IsNull == true) + { + Debug.Assert(false, "Could not load arrow cursor"); + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32ErrorCode)); + } + // + var lpWndClassEx = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = hCursor, + }; + + // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClassEx); + if (registerClassResult == 0) // failure + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + if (win32ErrorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CLASS_ALREADY_EXISTS) + { + Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); + } + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32ErrorCode)); + } + s_morphicArgbImageClassInfoExAtom = registerClassResult; + } + + /* create an instance of our native window */ + + var windowParams = new System.Windows.Forms.CreateParams() + { + ClassName = s_morphicArgbImageClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int + Caption = nativeWindowClassName, + Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP | PInvoke.User32.WindowStyles.WS_VISIBLE)), + ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*/ | PInvoke.User32.WindowStylesEx.WS_EX_TRANSPARENT), + //ClassStyle = ?, + X = x, + Y = y, + Width = width, + Height = height, + Parent = parentHWnd, + //Param = ?, + }; + + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception + try + { + result.CreateHandle(windowParams); + } + catch (PInvoke.Win32Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)ex.ErrorCode)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + + // since we are making the image visible by default, set its visible state to true + result._visible = true; + + return MorphicResult.OkResult(result); + } + + // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually + // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname + IntPtr classNameAsIntPtr; + bool classNameAsIntPtrRequiresFree = false; + if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUshort; + } + else + { + if (cp.ClassName is not null) + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + classNameAsIntPtrRequiresFree = true; + } + else + { + classNameAsIntPtr = IntPtr.Zero; + } + } + // + try + { + // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + if (handle == IntPtr.Zero) + { + var win32ErrorCode = Marshal.GetLastWin32Error(); + throw new System.ComponentModel.Win32Exception(win32ErrorCode); + } + + this.AssignHandle(handle); + } + finally + { + if (classNameAsIntPtrRequiresFree == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + // + + // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((PInvoke.User32.WindowMessage)msg) + { + case PInvoke.User32.WindowMessage.WM_CREATE: + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process + } + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); + } + + // NOTE: this WndProc method processes all messages after the initial creation of the window + protected override void WndProc(ref System.Windows.Forms.Message m) + { + IntPtr? result = null; + + switch ((PInvoke.User32.WindowMessage)m.Msg) + { + case PInvoke.User32.WindowMessage.WM_NCDESTROY: + { + // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + _ = Windows.Win32.PInvoke.BufferedPaintUnInit(); + + // NOTE: we pass along this message (i.e. we don't return a "handled" result) + } + break; + case PInvoke.User32.WindowMessage.WM_NCPAINT: + { + // we suppress all painting of the non-client areas (so that we can have a transparent window) + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + default: + break; + } + + // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message + if (result is not null) + { + m.Result = result.Value!; + } + else + { + // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, + // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled + // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 + m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage)m.Msg, m.WParam, m.LParam); + //base.WndProc(ref m); // DO NOT USE: this causes crashes (when other native windows are capturing/processing/passing along messages) + } + } + + // + + public System.Drawing.Bitmap? GetBitmap() + { + return _sourceBitmap; + } + + public interface ISetBitmapError + { + public record CannotRequestWindowRedraw(IRequestRedrawError InnerError) : ISetBitmapError; + public record OtherException(Exception Exception) : ISetBitmapError; + public record Win32Error(uint Win32ErrorCode) : ISetBitmapError; + public record WindowSizeIsZero : ISetBitmapError; + } + public MorphicResult SetBitmap(System.Drawing.Bitmap? bitmap) + { + _sourceBitmap = bitmap; + var recreateSizedBitmapResult = this.CreateAndCacheSizedBitmap(bitmap); + if (recreateSizedBitmapResult.IsError == true) + { + switch (recreateSizedBitmapResult.Error!) + { + case ICreateAndCacheSizedBitmapError.OtherException(var ex): + return MorphicResult.ErrorResult(new ISetBitmapError.OtherException(ex)); + case ICreateAndCacheSizedBitmapError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ISetBitmapError.Win32Error(win32ErrorCode)); + case ICreateAndCacheSizedBitmapError.WindowSizeIsZero: + return MorphicResult.ErrorResult(new ISetBitmapError.WindowSizeIsZero()); + default: + throw new MorphicUnhandledErrorException(); + } + } + + var requestRedrawResult = this.RequestRedraw(); + if (requestRedrawResult.IsError == true) + { + var innerError = requestRedrawResult.Error!; + return MorphicResult.ErrorResult(new ISetBitmapError.CannotRequestWindowRedraw(innerError)); + } + + return MorphicResult.OkResult(); + } + + public interface ISetPositionAndSizeError + { + public record CannotRequestWindowRedraw(IRequestRedrawError InnerError) : ISetPositionAndSizeError; + public record CouldNotResizeWindow(uint Win32ErrorCode) : ISetPositionAndSizeError; + public record OtherException(Exception Exception) : ISetPositionAndSizeError; + public record WindowSizeIsZero : ISetPositionAndSizeError; + public record Win32Error(uint Win32ErrorCode) : ISetPositionAndSizeError; + } + // + public MorphicResult SetPositionAndSize(Windows.Win32.Foundation.RECT rect) + { + // set the new window position (including size); we must resize the window before recreating the sized bitmap (which will be sized to the updated size) + var setWindowPosResult = Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + if (setWindowPosResult == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ISetPositionAndSizeError.CouldNotResizeWindow((uint)win32ErrorCode)); + } + + var createAndCacheSizedBitmapResult = this.CreateAndCacheSizedBitmap(_sourceBitmap); + if (createAndCacheSizedBitmapResult.IsError == true) + { + switch (createAndCacheSizedBitmapResult.Error!) + { + case ICreateAndCacheSizedBitmapError.OtherException(var ex): + return MorphicResult.ErrorResult(new ISetPositionAndSizeError.OtherException(ex)); + case ICreateAndCacheSizedBitmapError.WindowSizeIsZero: + return MorphicResult.ErrorResult(new ISetPositionAndSizeError.WindowSizeIsZero()); + case ICreateAndCacheSizedBitmapError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ISetPositionAndSizeError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + + var requestRedrawResult = this.RequestRedraw(); + if (requestRedrawResult.IsError == true) + { + var innerError = requestRedrawResult.Error!; + return MorphicResult.ErrorResult(new ISetPositionAndSizeError.CannotRequestWindowRedraw(innerError)); + } + + return MorphicResult.OkResult(); + } + + public interface ISetVisibleError + { + public record Win32Error(uint Win32ErrorCode) : ISetVisibleError; + } + // + public MorphicResult SetVisible(bool value) + { + if (_visible != value) + { + _visible = value; + + var windowStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE); + if (windowStyle == IntPtr.Zero) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ISetVisibleError.Win32Error((uint)win32ErrorCode)); + } + nint newWindowStyle; + if (_visible == true) + { + newWindowStyle = (nint)windowStyle | (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; + } + else + { + newWindowStyle = (nint)windowStyle & ~(nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_VISIBLE; + } + var setWindowLongPtrResult = PInvokeExtensions.SetWindowLongPtr_IntPtr((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_STYLE, newWindowStyle); + if (setWindowLongPtrResult == 0) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ISetVisibleError.Win32Error((uint)win32ErrorCode)); + } + } + + return MorphicResult.OkResult(); + } + + public interface ICreateAndCacheSizedBitmapError + { + public record OtherException(Exception Ex) : ICreateAndCacheSizedBitmapError; + public record Win32Error(uint Win32ErrorCode) : ICreateAndCacheSizedBitmapError; + public record WindowSizeIsZero : ICreateAndCacheSizedBitmapError; + } + private MorphicResult CreateAndCacheSizedBitmap(System.Drawing.Bitmap? bitmap) + { + if (bitmap is not null) + { + var getCurrentSizeResult = this.GetCurrentSize(); + if (getCurrentSizeResult.IsSuccess == true) + { + var currentSize = getCurrentSizeResult.Value!; + if (currentSize.Width == 0 || currentSize.Height == 0) + { + return MorphicResult.ErrorResult(new ICreateAndCacheSizedBitmapError.WindowSizeIsZero()); + } + // + try + { + var sizedBitmap = new System.Drawing.Bitmap(bitmap, currentSize); + _sizedBitmap = sizedBitmap; + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateAndCacheSizedBitmapError.OtherException(ex)); + } + + return MorphicResult.OkResult(); + } + else + { + switch (getCurrentSizeResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateAndCacheSizedBitmapError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + } + else + { + _sizedBitmap = null; + return MorphicResult.OkResult(); + } + } + + private MorphicResult GetCurrentSize() + { + var getWindowRectResult = Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out var rect); + if (getWindowRectResult == 0) + { + var win32Error = (uint)System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32Error)); + } + + var result = new System.Drawing.Size(rect.right - rect.left, rect.bottom - rect.top); + return MorphicResult.OkResult(result); + } + + // + + internal interface IRequestRedrawError + { + public record CouldNotInvalidateWindow : IRequestRedrawError; + public record CouldNotUpdateLayeredPainting(IUpdateLayeredPaintingError InnerError) : IRequestRedrawError; + } + private MorphicResult RequestRedraw() + { + // update our layered bitmap + var updateLayeredPaintingResult = this.UpdateLayeredPainting(); + if (updateLayeredPaintingResult.IsError == true) + { + var innerError = updateLayeredPaintingResult.Error!; + return MorphicResult.ErrorResult(new IRequestRedrawError.CouldNotUpdateLayeredPainting(innerError)); + } + + // invalidate the window + var redrawWindowSuccess = PInvokeExtensions.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, /*Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ERASE | */Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_INVALIDATE/* | Windows.Win32.Graphics.Gdi.REDRAW_WINDOW_FLAGS.RDW_ALLCHILDREN*/); + if (redrawWindowSuccess == false) + { + return MorphicResult.ErrorResult(new IRequestRedrawError.CouldNotInvalidateWindow()); + } + + return MorphicResult.OkResult(); + } + + internal interface IUpdateLayeredPaintingError + { + public record CouldNotCreateCompatibleDeviceContext : IUpdateLayeredPaintingError; + public record CouldNotCreateGdiBitmap(Exception InnerException) : IUpdateLayeredPaintingError; + public record CouldNotGetCurrentSize(uint Win32ErrorCode) : IUpdateLayeredPaintingError; + public record CouldNotGetDeviceContextOfOwner : IUpdateLayeredPaintingError; + public record CouldNotGetOwner : IUpdateLayeredPaintingError; + public record CouldNotSelectSizedBitmapInSourceDeviceContext : IUpdateLayeredPaintingError; + public record CouldNotUpdateLayeredWindow(uint Win32ErrorCode) : IUpdateLayeredPaintingError; + } + private MorphicResult UpdateLayeredPainting() + { + var ownerHWnd = Windows.Win32.PInvoke.GetWindow((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.UI.WindowsAndMessaging.GET_WINDOW_CMD.GW_OWNER); + if (ownerHWnd == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotGetOwner()); + } + // + var getCurrentSizeResult = this.GetCurrentSize(); + if (getCurrentSizeResult.IsError == true) + { + switch (getCurrentSizeResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + Debug.Assert(false, "Could not get current size of native window; win32 error: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotGetCurrentSize(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var size = getCurrentSizeResult.Value!; + // + var sizedBitmap = _sizedBitmap; + + if (sizedBitmap is not null) + { + // create a GDI bitmap from the Bitmap (using (0, 0, 0, 0) as the color of the ARGB background i.e. transparent) + Windows.Win32.Graphics.Gdi.HGDIOBJ sizedBitmapPointer; + try + { + sizedBitmapPointer = (Windows.Win32.Graphics.Gdi.HGDIOBJ)sizedBitmap.GetHbitmap(System.Drawing.Color.FromArgb(0)); + } + catch (Exception ex) + { + Debug.Assert(false, "Could not create GDI bitmap object from the sized bitmap."); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotCreateGdiBitmap(ex)); + } + try + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc + var ownerDC = Windows.Win32.PInvoke.GetDC(ownerHWnd); + if (ownerDC.Value == IntPtr.Zero) + { + Debug.Assert(false, "Could not get owner DC so that we can draw the icon bitmap."); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotGetDeviceContextOfOwner()); + } + try + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createcompatibledc + var sourceDC = Windows.Win32.PInvoke.CreateCompatibleDC(ownerDC); + if (sourceDC.Value == IntPtr.Zero) + { + Debug.Assert(false, "Could not get create compatible DC for screen DC so that we can draw the icon bitmap."); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotCreateCompatibleDeviceContext()); + } + try + { + // select our bitmap in the source DC + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-selectobject + var oldSourceDCObject = Windows.Win32.PInvoke.SelectObject(sourceDC, sizedBitmapPointer); + if (oldSourceDCObject.Value == new IntPtr(-1) /*HGDI_ERROR*/) + { + Debug.Assert(false, "Could not select the icon bitmap GDI object to update the layered window with the alpha-blended bitmap."); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotSelectSizedBitmapInSourceDeviceContext()); + } + try + { + // configure our blend function to blend the bitmap into its background + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-blendfunction + var blendfunction = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() + { + BlendOp = (byte)Windows.Win32.PInvoke.AC_SRC_OVER, /* the only available blend op, this will place the source bitmap over the destination bitmap based on the alpha values of the source pixels */ + BlendFlags = 0, /* must be zero */ + SourceConstantAlpha = 255, /* use per-pixel alpha values */ + AlphaFormat = (byte)Windows.Win32.PInvoke.AC_SRC_ALPHA, /* the bitmap has an alpha channel; it MUST be a 32bpp bitmap */ + }; + var sourcePoint = new System.Drawing.Point(0, 0); + var flags = Windows.Win32.UI.WindowsAndMessaging.UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA; // this flag indicates the blendfunction should be used as the blend function + //var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, position/* captured position of our window */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-updatelayeredwindow + var updateLayeredWindowSuccess = Windows.Win32.PInvoke.UpdateLayeredWindow((Windows.Win32.Foundation.HWND)this.Handle, ownerDC, null/* current position is not changing */, size, sourceDC, sourcePoint, (Windows.Win32.Foundation.COLORREF)0/* unused COLORREF*/, blendfunction, flags); + if (updateLayeredWindowSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not update the layered window with the alpha-blended bitmap; win32 error code: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult(new IUpdateLayeredPaintingError.CouldNotUpdateLayeredWindow((uint)win32ErrorCode)); + } + } + finally + { + // restore the old source object for the source DC + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-selectobject + var selectObjectResult = Windows.Win32.PInvoke.SelectObject(sourceDC, oldSourceDCObject); + if (selectObjectResult == new IntPtr(-1) /*HGDI_ERROR*/) + { + Debug.Assert(false, "Could not restore the screen's compatible DC to its previous object after attempting to update the layered window."); + } + } + } + finally + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-deletedc + var deleteDCSuccess = Windows.Win32.PInvoke.DeleteDC(sourceDC); + Debug.Assert(deleteDCSuccess == true, "Could not delete the compatible DC for the owner DC."); + } + + } + finally + { + //see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc + var releaseDcResult = Windows.Win32.PInvoke.ReleaseDC(ownerHWnd, ownerDC); + Debug.Assert(releaseDcResult == 1, "Could not release owner DC."); + } + } + finally + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-deleteobject + var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(sizedBitmapPointer); + Debug.Assert(deleteObjectSuccess == true, "Could not delete the GDI bitmap object which was created from the icon bitmap"); + } + } + else + { + // NOTE: we do not support erasing the bitmap once it's created, so there is nothing to do here; the caller may hide the image by setting its visible state to true + } + + // if we reach here, the operation was successful + return MorphicResult.OkResult(); + } +} diff --git a/Morphic.Core/MorphicAssociatedValueEnum.cs b/Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs similarity index 62% rename from Morphic.Core/MorphicAssociatedValueEnum.cs rename to Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs index 43cd2cb1..c787849c 100644 --- a/Morphic.Core/MorphicAssociatedValueEnum.cs +++ b/Morphic.Controls/TrayButton/Windows11/ICreateNewError.cs @@ -1,10 +1,10 @@ -// Copyright 2021 Raising the Floor - International +// Copyright 2020-2025 Raising the Floor - US, Inc. // // Licensed under the New BSD license. You may not use this file except in // compliance with this License. // // You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt // // The R&D leading to these results received funding from the: // * Rehabilitation Services Administration, US Dept. of Education under @@ -23,17 +23,13 @@ using System; -public abstract record MorphicAssociatedValueEnum where TValue : struct, Enum -{ - public TValue Value { get; private set; } - - protected MorphicAssociatedValueEnum(TValue value) - { - this.Value = value; - } +namespace Morphic.Controls.TrayButton.Windows11; - public static bool IsMember(TValue value) - { - return MorphicEnum.IsMember(value); - } -} \ No newline at end of file +public interface ICreateNewError +{ + public record CannotFitOnTaskbar : ICreateNewError; + public record CouldNotFindTaskbarRelatedHandle: ICreateNewError; + public record CouldNotWireUpWatchEvents: ICreateNewError; + public record OtherException(Exception exception) : ICreateNewError; + public record Win32Error(uint win32ErrorCode) : ICreateNewError; +} diff --git a/Morphic.Controls/TrayButton/Windows11/TrayButton.cs b/Morphic.Controls/TrayButton/Windows11/TrayButton.cs new file mode 100644 index 00000000..8aeff01a --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows11/TrayButton.cs @@ -0,0 +1,337 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Diagnostics; + +namespace Morphic.Controls.TrayButton.Windows11; + +internal class TrayButton : IDisposable +{ + private bool disposedValue; + + private System.Drawing.Bitmap? _bitmap = null; + private string? _text = null; + private TrayButtonVisibility _visibility = TrayButtonVisibility.Hidden; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + private TrayButtonNativeWindow? _nativeWindow = null; + + public System.Drawing.Rectangle? PositionAndSize + { + get + { + return _nativeWindow?.PositionAndSize; + } + } + + private System.Windows.Forms.Timer _reattemptShowTaskbarButtonTimer; + private static readonly TimeSpan REATTEMPT_SHOW_TASKBAR_BUTTON_INTERVAL_TIMESPAN = new TimeSpan(0, 0, 10); + + internal TrayButton() + { + } + + // + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + _nativeWindow?.Dispose(); + + _reattemptShowTaskbarButtonTimer?.Dispose(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // [none] + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~TrayButton() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public System.Drawing.Bitmap? Bitmap + { + get + { + return _bitmap; + } + set + { + _bitmap = value; + + //OBSERVATION: we do not return an error if the bitmap cannot be set + if (_nativeWindow is not null) + { + var setBitmapResult = _nativeWindow!.SetBitmap(_bitmap); + if (setBitmapResult.IsError == true) + { + // NOTE: in the future, we may want to consider capturing the error + Debug.Assert(false, "Could not set bitmap."); + } + } + } + } + + public string? Text + { + get + { + return _text; + } + set + { + _text = value; + + if (_nativeWindow is not null) + { + var setTextResult = _nativeWindow!.SetText(_text); + if (setTextResult.IsError == true) + { + // NOTE: in the future, we may want to consider capturing the error + Debug.Assert(false, "Could not set text."); + } + } + } + } + + public TrayButtonVisibility Visibility + { + get + { + return _visibility; + } + set + { + switch (value) + { + case TrayButtonVisibility.Visible: + if (_visibility == TrayButtonVisibility.Hidden) + { + var showResult = this.Show(); + if (showResult.IsError == true) + { + // NOTE: we could try to handle various IShowError error codes here + // + // NOTE: as a fallback, when "show" fails we set a timer to try to show the button + Debug.Assert(false, "Could not show Morphic icon (taskbar button) on taskbar; setting Visibility to .PendingVisible"); + _visibility = TrayButtonVisibility.PendingVisible; + + // start a timer on the new instance, to resurface the Morphic tray button icon from time to time (just in case it gets hidden under the taskbar) + // NOTE: we use a Windows Forms timers here (instead of a system timer) so that the function gets called on the UI thread (or at least the same thread which called this function) + _reattemptShowTaskbarButtonTimer = new() + { + Interval = (int)REATTEMPT_SHOW_TASKBAR_BUTTON_INTERVAL_TIMESPAN.TotalMilliseconds, + }; + _reattemptShowTaskbarButtonTimer.Tick += this.ReattemptShowTaskButtonTimer_Tick; + _reattemptShowTaskbarButtonTimer.Start(); + } + } + break; + case TrayButtonVisibility.PendingVisible: + throw new ArgumentException("State 'PendingVisible' is invalid for the Visibility Set operation"); + case TrayButtonVisibility.Hidden: + if (_visibility != TrayButtonVisibility.Hidden) + { + this.Hide(); + } + break; + } + } + } + + // + + // NOTE: the Show() method is only concerned with immediately showing the window (by creating the native window); the Visibility property is separate, a state which indicates if the control SHOULD be shown (and whether or not it's currently shown or _trying_ to be shown (i.e. pending)) + public interface IShowError + { + public record CouldNotCreateWindow(ICreateNewError InnerError) : IShowError; + public record CouldNotSetBitmap(TrayButtonNativeWindow.ISetBitmapError InnerError) : IShowError; + public record CouldNotSetText(TrayButtonNativeWindow.IUpdateTooltipTextAndTrackingError InnerError) : IShowError; + public record OtherError : IShowError; + } + // + public MorphicResult Show() + { + if (_nativeWindow is null) + { + var createNativeWindowResult = this.CreateNativeWindow(); + if (createNativeWindowResult.IsError == true) + { + switch (createNativeWindowResult.Error!) + { + case ICreateNativeWindowError.AlreadyExists: + Debug.Assert(false, "Race condition: native window already exists"); + return MorphicResult.ErrorResult(new IShowError.OtherError()); + case ICreateNativeWindowError.CreateFailed(ICreateNewError innerError): + return MorphicResult.ErrorResult(new IShowError.CouldNotCreateWindow(innerError)); + case ICreateNativeWindowError.CouldNotSetBitmap(var innerError): + return MorphicResult.ErrorResult(new IShowError.CouldNotSetBitmap(innerError)); + case ICreateNativeWindowError.CouldNotSetText(var innerError): + return MorphicResult.ErrorResult(new IShowError.CouldNotSetText(innerError)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var nativeWindow = createNativeWindowResult.Value!; + + // store the reference to our new native window + _nativeWindow = nativeWindow; + + // if we created the window, it is now "visible" from the perspective of the TrayButton + // NOTE: the native window itself will show/hide depending on the topmost state of the taskbar; our control's "visibility" is strictly concerned with whether or not the control is set to be visible right now (and if it is, if it's actually visible or just trying to becoming (pending) visible) + _visibility = TrayButtonVisibility.Visible; + } + + return MorphicResult.OkResult(); + } + + public void Hide() + { + // NOTE: if we are currently "pending visible" (i.e. our timer is live), then cancel that now + // NOTE: there is a possibility that the timer is currently executing when we dispose of it + if (_reattemptShowTaskbarButtonTimer is not null) + { + _reattemptShowTaskbarButtonTimer?.Dispose(); + _reattemptShowTaskbarButtonTimer = null; + } + + if (_nativeWindow is not null) + { + _nativeWindow?.Dispose(); + _nativeWindow = null; + } + + _visibility = TrayButtonVisibility.Hidden; + } + + // + + //// NOTE: this may be uncommented if the functionality is required + // public void SuppressTaskbarButtonResurfaceChecks(bool suppress) + // { + // _nativeWindow?.SuppressTaskbarButtonResurfaceChecks(suppress); + // } + + // + + private interface ICreateNativeWindowError + { + public record AlreadyExists : ICreateNativeWindowError; + public record CouldNotSetBitmap(TrayButtonNativeWindow.ISetBitmapError InnerError) : ICreateNativeWindowError; + public record CouldNotSetText(TrayButtonNativeWindow.IUpdateTooltipTextAndTrackingError InnerError) : ICreateNativeWindowError; + public record CreateFailed(ICreateNewError InnerError) : ICreateNativeWindowError; + } + // + private MorphicResult CreateNativeWindow() + { + // if our native window already exists, return an error + if (_nativeWindow is not null) + { + return MorphicResult.ErrorResult(new ICreateNativeWindowError.AlreadyExists()); + } + + // create the native window + var createNewResult = TrayButtonNativeWindow.CreateNew(); + if (createNewResult.IsError) + { + var innerError = createNewResult.Error!; + return MorphicResult.ErrorResult(new ICreateNativeWindowError.CreateFailed(innerError)); + } + var nativeWindow = createNewResult.Value!; + + // wire up the native window's MouseUp event (so that we bubble up its event to our creator) + nativeWindow.MouseUp += (s, e) => + { + this.MouseUp?.Invoke(s, e); + }; + + // set the bitmap ("icon") for the native window + var setBitmapResult = nativeWindow.SetBitmap(_bitmap); + if (setBitmapResult.IsError == true) + { + nativeWindow.Dispose(); + // + var innerError = setBitmapResult.Error!; + return MorphicResult.ErrorResult(new ICreateNativeWindowError.CouldNotSetBitmap(innerError)); + } + // + // set the (tooltip) text for the native window + var setTextResult = nativeWindow.SetText(_text); + if (setTextResult.IsError == true) + { + nativeWindow.Dispose(); + // + var innerError = setTextResult.Error!; + return MorphicResult.ErrorResult(new ICreateNativeWindowError.CouldNotSetText(innerError)); + } + + return MorphicResult.OkResult(nativeWindow); + } + + // NOTE: if we tried to show the taskbar button but the operation failed, keep retrying + // NOTE: we use a Windows Forms timer here instead of a system timer (in an effort to keep the .Show() function call on the main/UI thread) + private void ReattemptShowTaskButtonTimer_Tick(object? sender, EventArgs e) + { + if (_visibility == TrayButtonVisibility.PendingVisible) + { + var showResult = this.Show(); + if (showResult.IsSuccess == true) + { + // we were successfully able to show the taskbar button; the reattempt timer is not longer necessary + _reattemptShowTaskbarButtonTimer?.Dispose(); + _reattemptShowTaskbarButtonTimer = null; + } + } + else + { + Debug.Assert(false, "ReattemptShowTaskButtonTimerCallback was called, but Visibility is not currently set to .PendingVisible; value: " + _visibility.ToString()); + _reattemptShowTaskbarButtonTimer?.Dispose(); + _reattemptShowTaskbarButtonTimer = null; + } + } +} diff --git a/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs b/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs new file mode 100644 index 00000000..56da8d3c --- /dev/null +++ b/Morphic.Controls/TrayButton/Windows11/TrayButtonNativeWindow.cs @@ -0,0 +1,1768 @@ +// Copyright 2020-2025 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-controls-lib-cs/blob/main/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Morphic.Controls.TrayButton.Windows11; + +internal class TrayButtonNativeWindow : System.Windows.Forms.NativeWindow, IDisposable +{ + private bool disposedValue; + + private static ushort? s_morphicTrayButtonClassInfoExAtom = null; + + private System.Windows.Visibility _visibility; + private bool _taskbarIsTopmost; + + private System.Threading.Timer? _resurfaceTaskbarButtonTimer; + private static readonly TimeSpan RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN = new TimeSpan(0, 0, 30); + + private ArgbImageNativeWindow? _argbImageNativeWindow = null; + + private IntPtr _tooltipWindowHandle; + private bool _tooltipInfoAdded = false; + private string? _tooltipText; + + private Windows.Win32.Foundation.RECT _trayButtonPositionAndSize; + public System.Drawing.Rectangle PositionAndSize + { + get + { + return new(_trayButtonPositionAndSize.X, _trayButtonPositionAndSize.Y, _trayButtonPositionAndSize.Width, _trayButtonPositionAndSize.Height); + } + } + + private IntPtr _locationChangeWindowEventHook = IntPtr.Zero; + private PInvokeExtensions.WinEventProc? _locationChangeWindowEventProc = null; + + private IntPtr _objectReorderWindowEventHook = IntPtr.Zero; + private PInvokeExtensions.WinEventProc? _objectReorderWindowEventProc = null; + + [Flags] + private enum TrayButtonVisualStateFlags + { + None = 0, // normal visual state + Hover = 1, + LeftButtonPressed = 2, + RightButtonPressed = 4 + } + private TrayButtonVisualStateFlags _visualState = TrayButtonVisualStateFlags.None; + + private const byte ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE = 1; + + public event System.Windows.Forms.MouseEventHandler? MouseUp; + + private TrayButtonNativeWindow() + { + } + + // + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // dispose managed state (managed objects) + if (_objectReorderWindowEventHook != IntPtr.Zero) + { + PInvokeExtensions.UnhookWinEvent(_objectReorderWindowEventHook); + } + if (_locationChangeWindowEventHook != IntPtr.Zero) + { + PInvokeExtensions.UnhookWinEvent(_locationChangeWindowEventHook); + } + + _argbImageNativeWindow?.Dispose(); + + _resurfaceTaskbarButtonTimer?.Dispose(); + } + + // free unmanaged resources (unmanaged objects) and override finalizer + this.DestroyHandle(); + _ = this.DestroyTooltipWindow(); + + // set large fields to null + // [none] + + disposedValue = true; + } + } + + // NOTE: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + ~TrayButtonNativeWindow() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + // + + public static MorphicResult CreateNew() + { + var result = new TrayButtonNativeWindow(); + + /* register a custom native window class for our Morphic Tray Button (or refer to the already-registered class, if we captured it earlier in the application's execution) */ + const string nativeWindowClassName = "Morphic-TrayButton"; + // + if (s_morphicTrayButtonClassInfoExAtom is null) + { + // register our control's custom native window class + nint pointerToWndProcCallback; + try + { + pointerToWndProcCallback = Marshal.GetFunctionPointerForDelegate(new PInvokeExtensions.WndProc(result.WndProcCallback)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + // + var hCursor = Windows.Win32.PInvoke.LoadCursor(Windows.Win32.Foundation.HINSTANCE.Null, Windows.Win32.PInvoke.IDC_ARROW); + if (hCursor.IsNull == true) + { + Debug.Assert(false, "Could not load arrow cursor"); + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32ErrorCode)); + } + // + var lpWndClassEx = new PInvokeExtensions.WNDCLASSEX + { + cbSize = (uint)Marshal.SizeOf(typeof(PInvokeExtensions.WNDCLASSEX)), + lpfnWndProc = pointerToWndProcCallback, + lpszClassName = nativeWindowClassName, + hCursor = hCursor, + }; + + // NOTE: RegisterClassEx returns an ATOM (or 0 if the call failed) + var registerClassResult = PInvokeExtensions.RegisterClassEx(ref lpWndClassEx); + if (registerClassResult == 0) // failure + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + if (win32ErrorCode == (int)Windows.Win32.Foundation.WIN32_ERROR.ERROR_CLASS_ALREADY_EXISTS) + { + Debug.Assert(false, "Class was already registered; we should have recorded this ATOM, and we cannot proceed"); + } + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)win32ErrorCode)); + } + s_morphicTrayButtonClassInfoExAtom = registerClassResult; + } + + /* calculate the initial position of the tray button */ + var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(null); + if (calculatePositionResult.IsError) + { + switch (calculatePositionResult.Error!) + { + case ICalculatePositionAndSizeForTrayButtonError.CouldNotFindTaskbarRelatedHandle: + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotFindTaskbarRelatedHandle()); + case ICalculatePositionAndSizeForTrayButtonError.CannotFitOnTaskbar: + return MorphicResult.ErrorResult(new ICreateNewError.CannotFitOnTaskbar()); + case ICalculatePositionAndSizeForTrayButtonError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var trayButtonPositionAndSize = calculatePositionResult.Value!; + if (trayButtonPositionAndSize.Width == 0 || trayButtonPositionAndSize.Height == 0) + { + Debug.Assert(false, "Tray button position calculated, but it's zero pixels in size"); + return MorphicResult.ErrorResult(new ICreateNewError.CannotFitOnTaskbar()); + } + // + // capture our initial position and size + result._trayButtonPositionAndSize = trayButtonPositionAndSize; + + /* get the handle for the taskbar; it will be the owner of our native window (so that our window sits above it in the zorder) */ + // NOTE: we will still need to push our window to the front of its owner's zorder stack in some circumstances, as certain actions (such as popping up the task list balloons above the task bar) will reorder the taskbar's zorder and push us behind the taskbar + // NOTE: making the taskbar our owner has the side-effect of putting our window above full-screen applications (even though our window is not itself "always on top"); we will need to hide our window whenever a window goes full-screen on the same monitor (and re-show our window whenever the window exits full-screen mode) + var getTaskbarHandleResult = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + if (getTaskbarHandleResult.IsError == true) + { + switch (getTaskbarHandleResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var taskbarHandle = getTaskbarHandleResult.Value!; + if (taskbarHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotFindTaskbarRelatedHandle()); + } + + // capture the current state of the taskbar; this is combined with the visibility value to determine whether or not the window is actually visible to the user + var getTaskbarIsTopmostResult = TrayButtonNativeWindow.GetTaskbarIsTopmost(); + if (getTaskbarIsTopmostResult.IsError == true) + { + switch (getTaskbarIsTopmostResult.Error!) + { + case IGetTaskbarIsTopmostError.CouldNotFindTaskbarRelatedHandle: + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotFindTaskbarRelatedHandle()); + case IGetTaskbarIsTopmostError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + result._taskbarIsTopmost = getTaskbarIsTopmostResult.Value!; + + + /* create an instance of our native window */ + + var windowParams = new System.Windows.Forms.CreateParams() + { + ClassName = s_morphicTrayButtonClassInfoExAtom.ToString(), // for simplicity, we pass the value of the custom class as its integer self but in string form; our CreateWindow function will parse this and convert it to an int + Caption = nativeWindowClassName, + Style = unchecked((int)(/*PInvoke.User32.WindowStyles.WS_CLIPSIBLINGS | */PInvoke.User32.WindowStyles.WS_POPUP /*| PInvoke.User32.WindowStyles.WS_TABSTOP*/ | PInvoke.User32.WindowStyles.WS_VISIBLE)), + ExStyle = (int)(PInvoke.User32.WindowStylesEx.WS_EX_LAYERED/* | PInvoke.User32.WindowStylesEx.WS_EX_TOOLWINDOW*//* | PInvoke.User32.WindowStylesEx.WS_EX_TOPMOST*/), + //ClassStyle = ?, + X = trayButtonPositionAndSize.left, + Y = trayButtonPositionAndSize.top, + Width = trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, + Height = trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top, + Parent = taskbarHandle.Value, + //Param = ?, + }; + + // NOTE: CreateHandle can throw InvalidOperationException, OutOfMemoryException or Win32Exception + try + { + result.CreateHandle(windowParams); + } + catch (PInvoke.Win32Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error((uint)ex.ErrorCode)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(new ICreateNewError.OtherException(ex)); + } + + // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) + // NOTE: an alpha value of 0 (0%) makes our window complete see-through but it has the side-effect of not capturing any mouse events; to counteract this, + // we set our "tranparent" alpha value to 1 instead. We will only use an alpha value of 0 when we want our window to be invisible and also not capture mouse events + var setBackgroundAlphaResult = TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)result.Handle, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE); + if (setBackgroundAlphaResult.IsError) + { + switch (setBackgroundAlphaResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(uint win32ErrorCode): + return MorphicResult.ErrorResult(new ICreateNewError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + + // since we are making the native window visible by default, set its visibility state + // NOTE: this native window's visibility state is separate from the TrayButton's visibility state; the TrayButton's state is the desired visible state from the user's perspective (and can report when the button cannot currently be drawn), whereas + // this native window's visibility state indicates whether or not the native control should be visible IF the taskbar is on top (i.e. not in a full-screen video scenario, etc.) + result._visibility = System.Windows.Visibility.Visible; + + // create an instance of the ArgbImageNativeWindow to hold our icon; we cannot draw the bitmap directly on this window as the bitmap would then be alpha-blended the same % as our background (instead of being independently blended over our window) + var argbImageNativeWindowResult = ArgbImageNativeWindow.CreateNew(result.Handle, windowParams.X, windowParams.Y, windowParams.Width, windowParams.Height); + if (argbImageNativeWindowResult.IsError == true) + { + result.Dispose(); + // + // NOTE: ArgbImageNativeWindow returns the same ICreateNewError errors, so we can just pass them along... + return MorphicResult.ErrorResult(argbImageNativeWindowResult.Error!); + } + result._argbImageNativeWindow = argbImageNativeWindowResult.Value!; + + /* wire up windows event hook listeners, to watch for events which require adjusting the zorder of our window */ + + // NOTE: we could provide the process handle and thread of processes/threads which we were interested in specifically, but for now we're interested in more than one window so we filter broadly + var locationChangeWindowEventProc = new PInvokeExtensions.WinEventProc(result.LocationChangeWindowEventProc); + var locationChangeWindowEventHook = PInvokeExtensions.SetWinEventHook( + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // start index + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_LOCATIONCHANGE, // end index + IntPtr.Zero, + locationChangeWindowEventProc, + 0, // process handle (0 = all processes on current desktop) + 0, // thread (0 = all existing threads on current desktop) + PInvokeExtensions.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | PInvokeExtensions.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS + ); + Debug.Assert(locationChangeWindowEventHook != IntPtr.Zero, "Could not wire up location change window event listener for tray button"); + if (locationChangeWindowEventHook == IntPtr.Zero) + { + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotWireUpWatchEvents()); + } + // + result._locationChangeWindowEventHook = locationChangeWindowEventHook; + // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine + result._locationChangeWindowEventProc = locationChangeWindowEventProc; + // + // + // + var objectReorderWindowEventProc = new PInvokeExtensions.WinEventProc(result.ObjectReorderWindowEventProc); + var objectReorderWindowEventHook = PInvokeExtensions.SetWinEventHook( + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_REORDER, // start index + PInvokeExtensions.WinEventHookType.EVENT_OBJECT_REORDER, // end index + IntPtr.Zero, + objectReorderWindowEventProc, + 0, // process handle (0 = all processes on current desktop) + 0, // thread (0 = all existing threads on current desktop) + PInvokeExtensions.WinEventHookFlags.WINEVENT_OUTOFCONTEXT | PInvokeExtensions.WinEventHookFlags.WINEVENT_SKIPOWNPROCESS + ); + Debug.Assert(objectReorderWindowEventHook != IntPtr.Zero, "Could not wire up object reorder window event listener for tray button"); + if (objectReorderWindowEventHook == IntPtr.Zero) + { + return MorphicResult.ErrorResult(new ICreateNewError.CouldNotWireUpWatchEvents()); + } + // + result._objectReorderWindowEventHook = objectReorderWindowEventHook; + // NOTE: we must capture the delegate so that it is not garbage collected; otherwise the native callbacks can crash the .NET execution engine + result._objectReorderWindowEventProc = objectReorderWindowEventProc; + + // create the tooltip window (although we won't provide it with any actual text until/unless the text is set + result._tooltipWindowHandle = result.CreateTooltipWindow(); + result._tooltipText = null; + + // start a timer on the new instance, to resurface the Morphic tray button icon from time to time (just in case it gets hidden under the taskbar) + result._resurfaceTaskbarButtonTimer = new(result.ResurfaceTaskButtonTimerCallback, null, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN, TrayButtonNativeWindow.RESURFACE_TASKBAR_BUTTON_INTERVAL_TIMESPAN); + + return MorphicResult.OkResult(result); + } + + // NOTE: the built-in CreateHandle function couldn't accept our custom class (an ATOM rather than a string) as input, so we have overridden CreateHandle and are calling CreateWindowEx manually + // NOTE: in some circumstances, it is possible that we are unable to create our window; our caller may want to consider retrying mechanism + public override void CreateHandle(System.Windows.Forms.CreateParams cp) + { + // NOTE: if cp.ClassName is a string parseable as a short unsigned integer, parse it into an unsigned short; otherwise use the string as the classname + IntPtr classNameAsIntPtr; + bool classNameAsIntPtrRequiresFree = false; + if (cp.ClassName is not null && ushort.TryParse(cp.ClassName, out var classNameAsUshort) == true) + { + classNameAsIntPtr = (IntPtr)classNameAsUshort; + } + else + { + if (cp.ClassName is not null) + { + classNameAsIntPtr = Marshal.StringToHGlobalUni(cp.ClassName); + classNameAsIntPtrRequiresFree = true; + } + else + { + classNameAsIntPtr = IntPtr.Zero; + } + } + // + try + { + // NOTE: CreateWindowEx will return IntPtr.Zero ("NULL") if it fails + var handle = PInvokeExtensions.CreateWindowEx( + (PInvoke.User32.WindowStylesEx)cp.ExStyle, + classNameAsIntPtr, + cp.Caption, + (PInvoke.User32.WindowStyles)cp.Style, + cp.X, + cp.Y, + cp.Width, + cp.Height, + cp.Parent, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + if (handle == IntPtr.Zero) + { + var win32ErrorCode = Marshal.GetLastWin32Error(); + throw new System.ComponentModel.Win32Exception(win32ErrorCode); + } + + this.AssignHandle(handle); + } + finally + { + if (classNameAsIntPtrRequiresFree == true) + { + Marshal.FreeHGlobal(classNameAsIntPtr); + } + } + } + + // + + // Listen to when the handle changes to keep the argb image native window synced + protected override void OnHandleChange() + { + base.OnHandleChange(); + + // NOTE: if we ever need to update our children (or other owned windows) to let them know that our handle had changed, this is where we would add that code + } + + // NOTE: during initial creation of the window, callbacks are sent to this delegated event; after creation, messages are captured by the WndProc function instead + private IntPtr WndProcCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch ((PInvoke.User32.WindowMessage)msg) + { + case PInvoke.User32.WindowMessage.WM_CREATE: + // NOTE: it may not technically be necessary for us to use buffered painting for this control since we're effectively just painting it with a single fill color--but + // we do so to maintain consistency with the ArgbImageNativeWindow class and other user-painted forms + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + if (Windows.Win32.PInvoke.BufferedPaintInit() != Windows.Win32.Foundation.HRESULT.S_OK) + { + // failed; abort + Debug.Assert(false, "Could not initialize buffered paint"); + return new IntPtr(-1); // abort window creation process + } + break; + default: + break; + } + + // pass all non-handled messages through to DefWindowProc + return PInvoke.User32.DefWindowProc(hWnd, (PInvoke.User32.WindowMessage)msg, wParam, lParam); + } + + // NOTE: this WndProc method processes all messages after the initial creation of the window + protected override void WndProc(ref System.Windows.Forms.Message m) + { + IntPtr? result = null; + + switch ((PInvoke.User32.WindowMessage)m.Msg) + { + case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_LBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (convertLParamResult.IsSuccess == true) + { + var hitPoint = convertLParamResult.Value!; + + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Left, 1, hitPoint.X, hitPoint.Y, 0); + Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); + } + else + { + switch (convertLParamResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + Debug.Assert(false, "Could not map tray button hit point to screen coordinates; win32 errcode: " + win32ErrorCode.ToString()); + break; + default: + throw new MorphicUnhandledErrorException(); + } + } + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSELEAVE: + { + // the cursor has left our tray button's window area; remove the hover state from our visual state + _visualState &= ~TrayButtonVisualStateFlags.Hover; + + // NOTE: as we aren't able to track mouseup when the cursor is outside of the button, we also remove the left/right button pressed states here + // (and then we check them again when the mouse moves back over the button) + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: + { + // NOTE: this message is raised while we are tracking (whereas the SETCURSOR WM_MOUSEMOVE is captured when the mouse cursor first enters the window) + // + // NOTE: if the cursor moves off of the tray button while the button is pressed, we would have removed the "pressed" focus as well as the "hover" focus + // because we can't track mouseup when the cursor is outside of the button; consequently we also need to check the mouse pressed state during + // mousemove so that we can re-visualize (re-set flags for) the pressed state as appropriate. + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) == 0) && ((m.WParam.ToInt64() & PInvokeExtensions.MK_LBUTTON) != 0)) + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + } + if (((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) == 0) && ((m.WParam.ToInt64() & PInvokeExtensions.MK_RBUTTON) != 0)) + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + } + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_NCDESTROY: + { + // NOTE: we are calling this in response to WM_NCDESTROY (instead of WM_DESTROY) + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintinit + var bufferedPaintUnInitResult = Windows.Win32.PInvoke.BufferedPaintUnInit(); + if (bufferedPaintUnInitResult != 0) + { + Debug.Assert(false, "Could not uninitialize buffered painting (in response to WM_NCDESTROY)"); + } + + // NOTE: we pass along this message (i.e. we don't return a "handled" result) + } + break; + case PInvoke.User32.WindowMessage.WM_NCPAINT: + { + // we suppress all painting of the non-client areas (so that we can have a transparent window) + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_PAINT: + { + // NOTE: we override the built-in paint functionality with our own Paint function + this.OnPaintWindowsMessage((Windows.Win32.Foundation.HWND)m.HWnd); + // + // return zero, indicating that we processed the message + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + var convertLParamResult = this.ConvertMouseMessageLParamToScreenPoint(m.LParam); + if (convertLParamResult.IsSuccess) + { + var hitPoint = convertLParamResult.Value!; + + var mouseArgs = new System.Windows.Forms.MouseEventArgs(System.Windows.Forms.MouseButtons.Right, 1, hitPoint.X, hitPoint.Y, 0); + Task.Run(() => this.MouseUp?.Invoke(this, mouseArgs)); + } + else + { + switch (convertLParamResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + Debug.Assert(false, "Could not map tray button hit point to screen coordinates; win32 errcode: " + win32ErrorCode.ToString()); + break; + default: + throw new MorphicUnhandledErrorException(); + } + } + + result = IntPtr.Zero; + } + break; + case PInvoke.User32.WindowMessage.WM_SETCURSOR: + { + // wParam: window handle + // lParam: low-order word is the high-test result for the cursor position; high-order word specifies the mouse message that triggered this event + + var hitTestResult = (uint)((m.LParam.ToInt64() >> 0) & 0xFFFF); + var mouseMsg = (uint)((m.LParam.ToInt64() >> 16) & 0xFFFF); + + // NOTE: for messages which we handle, we return "TRUE" (1) to halt further message processing; this may not technically be necessary + // see: https://learn.microsoft.com/en-us/windows/win32/menurc/wm-setcursor + switch ((PInvoke.User32.WindowMessage)mouseMsg) + { + case PInvoke.User32.WindowMessage.WM_LBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.LeftButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_LBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.LeftButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_MOUSEMOVE: + { + // if we are not yet tracking the mouse position (i.e. this is effectively "mouse enter"), then start tracking it now (so that we can capture its move out of our box) + // NOTE: we track whether or not we are tracking the mouse by analyzing the hover state of our visual state flags + if ((_visualState & TrayButtonVisualStateFlags.Hover) == 0) + { + // track mousehover (for tooltips) and mouseleave (to remove hover effect) + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-trackmouseevent + var eventTrack = PInvokeExtensions.TRACKMOUSEEVENT.CreateNew(PInvokeExtensions.TRACKMOUSEEVENTFlags.TME_LEAVE, this.Handle, PInvokeExtensions.HOVER_DEFAULT); + var trackMouseEventSuccess = PInvokeExtensions.TrackMouseEvent(ref eventTrack); + if (trackMouseEventSuccess == false) + { + // failed + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not set up tracking of tray button window area; win32 errcode: " + win32ErrorCode.ToString()); + } + + _visualState |= TrayButtonVisualStateFlags.Hover; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + } + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONDOWN: + { + _visualState |= TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = new IntPtr(1); + } + break; + case PInvoke.User32.WindowMessage.WM_RBUTTONUP: + { + _visualState &= ~TrayButtonVisualStateFlags.RightButtonPressed; + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + Debug.Assert(updateVisualStateAlphaResult.IsSuccess, "Could not update visual state."); + + result = new IntPtr(1); + } + break; + default: + // unhandled setcurosr mouse message + break; + } + } + break; + default: + break; + } + + // if we handled the message, return 'result'; otherwise, if we did not handle the message, call through to DefWindowProc to handle the message + if (result is not null) + { + m.Result = result.Value!; + } + else + { + // NOTE: per the Microsoft .NET documentation, we should call base.WndProc to process any events which we have not handled; however, + // in our testing, this led to frequent crashes. So instead, we follow the traditional pattern and call DefWindowProc to handle any events which we have not handled + // see: https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.nativewindow.wndproc?view=windowsdesktop-6.0 + m.Result = PInvoke.User32.DefWindowProc(m.HWnd, (PInvoke.User32.WindowMessage) m.Msg, m.WParam, m.LParam); + //base.WndProc(ref m); // DO NOT USE: this causes crashes (when other native windows are capturing/processing/passing along messages) + } + } + + // NOTE: this function may ONLY be called when responding to a WM_PAINT message + // NOTE: we do not return any error result from this function; instead, we log or assert errors and then just abort the paint attempt + private void OnPaintWindowsMessage(Windows.Win32.Foundation.HWND hWnd) + { + // create a device context for drawing; we must destroy this automatically in a finally block. We are effectively replicating the functionality of C++'s CPaintDC. + Windows.Win32.Graphics.Gdi.PAINTSTRUCT paintStruct; + // NOTE: we experienced significant issues using PInvoke.User32.BeginPaint (possibly due to its IntPtr result wrapper), so we have redeclared the BeginPaint function ourselves + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-beginpaint + var deviceContext = Windows.Win32.PInvoke.BeginPaint(hWnd, out paintStruct)!; + if (deviceContext == IntPtr.Zero) + { + // no display context is available + Debug.Assert(false, "Cannot paint TrayButton in response to WM_PAINT message; no display device context is available."); + return; + } + try + { + // NOTE: to avoid flickering, we use buffered painting to erase the background, fill the background with a single (white) brush, and then apply the painted area to the window in a single paint operation + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-beginbufferedpaint + Windows.Win32.Graphics.Gdi.HDC bufferedPaintDc; + var paintBufferHandle = Windows.Win32.PInvoke.BeginBufferedPaint(paintStruct.hdc, in paintStruct.rcPaint, Windows.Win32.UI.Controls.BP_BUFFERFORMAT.BPBF_TOPDOWNDIB, null, out bufferedPaintDc); + if (paintBufferHandle == IntPtr.Zero) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Cannot begin a buffered paint operation for TrayButton (when responding to a WM_PAINT message); win32 errcode: " + win32ErrorCode.ToString()); + return; + } + try + { + // NOTE: this is the section where we call all of our actual (buffered) paint operations + + // clear our window's background (i.e. the buffer background) + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-bufferedpaintclear + var bufferedPaintClearHresult = Windows.Win32.PInvoke.BufferedPaintClear(paintBufferHandle, paintStruct.rcPaint); + if (bufferedPaintClearHresult != Windows.Win32.Foundation.HRESULT.S_OK) + { + Debug.Assert(false, "Could not clear background of TrayButton window--using buffered clearing (when responding to a WM_Paint message); result: " + bufferedPaintClearHresult.ToString()); + return; + } + + // create a solid white brush + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createsolidbrush + var createSolidBrushResult = Windows.Win32.PInvoke.CreateSolidBrush((Windows.Win32.Foundation.COLORREF)0x00FFFFFF); + if (createSolidBrushResult == IntPtr.Zero) + { + Debug.Assert(false, "Could not create white brush to paint the background of the TrayButton window (when responding to a WM_Paint message)."); + return; + } + var whiteBrush = createSolidBrushResult; + // + try + { + int fillRectResult; + unsafe + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-fillrect + fillRectResult = Windows.Win32.PInvoke.FillRect(bufferedPaintDc, &paintStruct.rcPaint, whiteBrush); + } + Debug.Assert(fillRectResult != 0, "Could not fill highlight background of Tray icon with white brush"); + } + finally + { + // clean up the white solid brush we created for the fill operation + // see: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-deleteobject + var deleteObjectSuccess = Windows.Win32.PInvoke.DeleteObject(whiteBrush); + Debug.Assert(deleteObjectSuccess == true, "Could not delete white brush object used to highlight Tray icon"); + } + } + finally + { + // complete the buffered paint operation and free the buffered paint handle + // see: https://learn.microsoft.com/en-us/windows/win32/api/uxtheme/nf-uxtheme-endbufferedpaint + var endBufferedPaintHresult = Windows.Win32.PInvoke.EndBufferedPaint(paintBufferHandle, true /* copy buffer to DC, completing the paint operation */); + Debug.Assert(endBufferedPaintHresult == Windows.Win32.Foundation.HRESULT.S_OK, "Error while attempting to end buffered paint operation for TrayButton; hresult: " + endBufferedPaintHresult.ToString()); + } + } + finally + { + // mark the end of painting; this function must always be called when BeginPaint was called (and succeeded), and only after drawing is complete + // + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-endpaint + // NOTE: per the MSDN docs, this function never returns zero (so there is no result to check) + _ = Windows.Win32.PInvoke.EndPaint(hWnd, in paintStruct); + } + } + + public System.Windows.Visibility Visibility + { + get + { + return _visibility; + } + set + { + switch (value) + { + case System.Windows.Visibility.Visible: + case System.Windows.Visibility.Hidden: + // allowed + break; + case System.Windows.Visibility.Collapsed: + // not allowed + throw new ArgumentException("Visibility may not be set to Collapsed"); + default: + throw new ArgumentOutOfRangeException(); + } + + if (_visibility != value) + { + _visibility = value; + var updateVisibilityResult = this.UpdateVisibility(); + if (updateVisibilityResult.IsError == true) + { + // NOTE: we may want to consider parsing out errors here + Debug.Assert(false, "Could not update .Visibility"); + } + } + } + } + + private MorphicResult UpdateVisibility() + { + _argbImageNativeWindow?.SetVisible(this.ShouldWindowBeVisible()); + // + var updateVisualStateAlphaResult = this.UpdateVisualStateAlpha(); + if (updateVisualStateAlphaResult.IsError == true) + { + switch (updateVisualStateAlphaResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + + return MorphicResult.OkResult(); + } + + private bool ShouldWindowBeVisible() + { + return (_visibility == System.Windows.Visibility.Visible) && (_taskbarIsTopmost == true); + } + + private MorphicResult UpdateVisualStateAlpha() + { + // default to "Normal" visual state + Double highlightOpacity = 0.0; + + if (this.ShouldWindowBeVisible()) + { + if (((_visualState & TrayButtonVisualStateFlags.LeftButtonPressed) != 0) || + ((_visualState & TrayButtonVisualStateFlags.RightButtonPressed) != 0)) + { + highlightOpacity = 0.25; + } + else if ((_visualState & TrayButtonVisualStateFlags.Hover) != 0) + { + highlightOpacity = 0.1; + } + + var alpha = (byte)((double)255 * highlightOpacity); + var setBackgroundAlphaResult = TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, Math.Max(alpha, ALPHA_VALUE_FOR_TRANSPARENT_BUT_HIT_TESTABLE)); + if (setBackgroundAlphaResult.IsError == true) + { + switch (setBackgroundAlphaResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + } + else + { + // collapsed or hidden controls should be invisible + var setBackgroundAlphaResult = TrayButtonNativeWindow.SetBackgroundAlpha((Windows.Win32.Foundation.HWND)this.Handle, 0); + if (setBackgroundAlphaResult.IsError == true) + { + switch (setBackgroundAlphaResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + } + + return MorphicResult.OkResult(); + } + + private static MorphicResult SetBackgroundAlpha(Windows.Win32.Foundation.HWND handle, byte alpha) + { + // set the window's background transparency to 0% (in the range of a 0 to 255 alpha channel, with 255 being 100%) + var setLayeredWindowAttributesSuccess = Windows.Win32.PInvoke.SetLayeredWindowAttributes(handle, (Windows.Win32.Foundation.COLORREF)0, alpha, Windows.Win32.UI.WindowsAndMessaging.LAYERED_WINDOW_ATTRIBUTES_FLAGS.LWA_ALPHA); + if (setLayeredWindowAttributesSuccess == false) + { + var win32Error = (uint)System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error(win32Error)); + } + + return MorphicResult.OkResult(); + } + + // + + private void LocationChangeWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + // we cannot process a location change message if the hwnd is zero + if (hwnd == IntPtr.Zero) + { + return; + } + + // attempt to capture the class name for the window; if the window has already been destroyed, this will fail + string? className = null; + var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); + if (getWindowClassNameResult.IsError == true) + { + Debug.Assert(false, "Could not get window class name; has the window already been destroyed?"); + return; + } + className = getWindowClassNameResult.Value!; + + if (className == "TaskListThumbnailWnd" || className == "TaskListOverlayWnd") + { + // if the window being moved was one of the task list windows (i.e. the windows that pop up above the taskbar), then our zorder has probably been pushed down. To counteract this, we make sure our window is "TOPMOST" + // NOTE: in initial testing, we set the window to TOPMOST in the ExStyles during handle construction. This was not always successful in keeping the window topmost, however, possibly because the taskbar becomes "more" topmost sometimes. So we re-set the window zorder here instead (without activating the window). + this.BringTaskButtonTopmostWithoutActivating(); + } + else if (className == "Shell_TrayWnd"/* || className == "ReBarWindow32"*/ || className == "TrayNotifyWnd") + { + // if the window being moved was the taskbar or the taskbar's notification tray, recalculate and update our position + // NOTE: we might also consider watching for location changes of the task button container, but as we don't use it for position/size calculations at the present time we do not watch accordingly + var repositionResult = this.RecalculatePositionAndRepositionWindow(); + // NOTE: if we want to handle error cases of RecalculatePositionAndRepositionWindow, we can do so here. + Debug.Assert(repositionResult.IsSuccess, "Could not reposition Tray Button window"); + } + } + + // NOTE: just in case we miss any edge cases to resurface our button, we resurface it from time to time on a timer + private void ResurfaceTaskButtonTimerCallback(object? state) + { + this.BringTaskButtonTopmostWithoutActivating(); + } + + private void BringTaskButtonTopmostWithoutActivating() + { + var setWindowPosSuccess = Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, Windows.Win32.Foundation.HWND.HWND_TOPMOST, 0, 0, 0, 0, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOMOVE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOSIZE | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + if (setWindowPosSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not bring task button topmost; win32 error: " + win32ErrorCode.ToString()); + } + } + + private void ObjectReorderWindowEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint idEventThread, uint dwmsEventTime) + { + // we cannot process an object reorder message if the hwnd is zero + if (hwnd == IntPtr.Zero) + { + return; + } + + // attempt to capture the class name for the window; if the window has already been destroyed, this will fail + string? className = null; + var getWindowClassNameResult = TrayButtonNativeWindow.GetWindowClassName(hwnd); + if (getWindowClassNameResult.IsError == true) + { + Debug.Assert(false, "Could not get window class name; has the window already been destroyed?"); + return; + } + className = getWindowClassNameResult.Value!; + + // capture the desktop handle + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdesktopwindow + var desktopHandle = Windows.Win32.PInvoke.GetDesktopWindow(); + + // if the reordered window was either the taskbar or the desktop, update the _taskbarIsTopmost state; this will generally be triggered when an app goes full-screen (or full-screen mode is exited) + if (className == "Shell_TrayWnd" || hwnd == desktopHandle.Value) + { + // whenever the window ordering changes, resurface our control + this.BringTaskButtonTopmostWithoutActivating(); + + // determine if the taskbar is topmost; the taskbar's topmost flag is removed when an app goes full-screen and should cover the taskbar (e.g. a full-screen video) + var getTaskbarIsTopmostResult = TrayButtonNativeWindow.GetTaskbarIsTopmost(/*hwnd -- not passed in, since the handle could be the desktop */); + if (getTaskbarIsTopmostResult.IsError == true) + { + switch (getTaskbarIsTopmostResult.Error!) + { + case IGetTaskbarIsTopmostError.CouldNotFindTaskbarRelatedHandle: + Debug.Assert(false, "Could not determine if taskbar is topmost; taskbar-related handle could not be found."); + return; + case IGetTaskbarIsTopmostError.Win32Error(var win32ErrorCode): + Debug.Assert(false, "Could not determine if taskbar is topmost; win32 errcode: " + win32ErrorCode.ToString()); + return; + default: + throw new MorphicUnhandledErrorException(); + } + } + _taskbarIsTopmost = getTaskbarIsTopmostResult.Value!; + // + // NOTE: UpdateVisibility takes both the .Visibility property and the topmost state of the taskbar into consideration to determine whether or not to show the control + var updateVisibilityResult = this.UpdateVisibility(); + if (updateVisibilityResult.IsError == true) + { + // NOTE: we may want to consider parsing out errors here + Debug.Assert(false, "Could not update .Visibility"); + } + } + } + + private interface IGetTaskbarIsTopmostError + { + public record CouldNotFindTaskbarRelatedHandle : IGetTaskbarIsTopmostError; + public record Win32Error(uint Win32ErrorCode) : IGetTaskbarIsTopmostError; + } + // + private static MorphicResult GetTaskbarIsTopmost(Windows.Win32.Foundation.HWND? taskbarHWnd = null) + { + Windows.Win32.Foundation.HWND taskbarHandle; + if (taskbarHWnd is not null) + { + taskbarHandle = taskbarHWnd!.Value; + } + else + { + var getTaskbarHandleResult = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + if (getTaskbarHandleResult.IsError == true) + { + switch (getTaskbarHandleResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new IGetTaskbarIsTopmostError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + // + taskbarHandle = getTaskbarHandleResult.Value!; + if (taskbarHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new IGetTaskbarIsTopmostError.CouldNotFindTaskbarRelatedHandle()); + } + } + + var taskbarWindowExStyle = PInvokeExtensions.GetWindowLongPtr_IntPtr(taskbarHandle, Windows.Win32.UI.WindowsAndMessaging.WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + if (taskbarWindowExStyle == IntPtr.Zero) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new IGetTaskbarIsTopmostError.Win32Error((uint)win32ErrorCode)); + } + var taskbarIsTopmost = ((nint)taskbarWindowExStyle & (nint)Windows.Win32.UI.WindowsAndMessaging.WINDOW_EX_STYLE.WS_EX_TOPMOST) != 0; + + return MorphicResult.OkResult(taskbarIsTopmost); + } + + private interface IRecalculatePositionAndRepositionWindowError + { + public record CouldNotBringToTop(uint Win32ErrorCode) : IRecalculatePositionAndRepositionWindowError; + public record CouldNotCalculatePositionAndSizeForTrayButton(ICalculatePositionAndSizeForTrayButtonError InnerError) : IRecalculatePositionAndRepositionWindowError; + public record CouldNotPositionAndResizeBitmap(IPositionAndResizeBitmapError InnerError) : IRecalculatePositionAndRepositionWindowError; + public record CouldNotSetTooltip(IUpdateTooltipTextAndTrackingError InnerError) : IRecalculatePositionAndRepositionWindowError; + public record CouldNotSetWindowPosition(uint Win32ErrorCode) : IRecalculatePositionAndRepositionWindowError; + } + // + private MorphicResult RecalculatePositionAndRepositionWindow() + { + // first, reposition our control (NOTE: this will be required to subsequently determine the position of our bitmap) + var calculatePositionResult = TrayButtonNativeWindow.CalculatePositionAndSizeForTrayButton(this.Handle); + if (calculatePositionResult.IsError) + { + Debug.Assert(false, "Cannot calculate position for tray button"); + // + var innerError = calculatePositionResult.Error!; + return MorphicResult.ErrorResult(new IRecalculatePositionAndRepositionWindowError.CouldNotCalculatePositionAndSizeForTrayButton(innerError)); + } + var trayButtonPositionAndSize = calculatePositionResult.Value!; + // + var size = new System.Drawing.Size(trayButtonPositionAndSize.right - trayButtonPositionAndSize.left, trayButtonPositionAndSize.bottom - trayButtonPositionAndSize.top); + // + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos + var setWindowPosResult = Windows.Win32.PInvoke.SetWindowPos((Windows.Win32.Foundation.HWND)this.Handle, (Windows.Win32.Foundation.HWND)IntPtr.Zero, trayButtonPositionAndSize.left, trayButtonPositionAndSize.top, size.Width, size.Height, Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOZORDER | Windows.Win32.UI.WindowsAndMessaging.SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + if (setWindowPosResult == 0) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "SetWindowPos failed while trying to reposition TrayButton native window; win32 errcode: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult< IRecalculatePositionAndRepositionWindowError>(new IRecalculatePositionAndRepositionWindowError.CouldNotSetWindowPosition((uint)win32ErrorCode)); + } + // + // capture our updated position and size + _trayButtonPositionAndSize = trayButtonPositionAndSize; + + // once the control is repositioned, reposition the bitmap + var bitmap = _argbImageNativeWindow?.GetBitmap(); + if (bitmap is not null) + { + var positionAndResizeBitmapResult = this.PositionAndResizeBitmap(bitmap); + if (positionAndResizeBitmapResult.IsError == true) + { + Debug.Assert(false, "Could not position and resize bitmap."); + var innerError = positionAndResizeBitmapResult.Error!; + return MorphicResult.ErrorResult(new IRecalculatePositionAndRepositionWindowError.CouldNotPositionAndResizeBitmap(innerError)); + } + } + + // also reposition the tooltip's tracking rectangle + if (_tooltipText is not null) + { + var updateTooltipTextAndTrackingResult = this.UpdateTooltipTextAndTracking(); + if (updateTooltipTextAndTrackingResult.IsError == true) + { + Debug.Assert(false, "Could not update tooltip text"); + var innerError = updateTooltipTextAndTrackingResult.Error!; + return MorphicResult.ErrorResult(new IRecalculatePositionAndRepositionWindowError.CouldNotSetTooltip(innerError)); + } + } + + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-bringwindowtotop + var bringWindowToTopSuccess = Windows.Win32.PInvoke.BringWindowToTop((Windows.Win32.Foundation.HWND)this.Handle); + if (bringWindowToTopSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not bring tray button window to top; win32 errcode: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult(new IRecalculatePositionAndRepositionWindowError.CouldNotBringToTop((uint)win32ErrorCode)); + } + + return MorphicResult.OkResult(); + } + + private static MorphicResult GetWindowClassName(IntPtr hWnd) + { + System.Text.StringBuilder classNameBuilder = new(256); + var getClassNameResult = PInvokeExtensions.GetClassName(hWnd, classNameBuilder, classNameBuilder.Capacity); + if (getClassNameResult == 0) + { + var win32Error = Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32Error)); + } + + var classNameAsString = classNameBuilder.ToString(); + return MorphicResult.OkResult(classNameAsString); + } + + // + + public interface ISetBitmapError + { + public record CouldNotPositionAndResizeBitmap(IPositionAndResizeBitmapError InnerError) : ISetBitmapError; + public record CouldNotSetBitmapInArgbImageNativeWindow(ArgbImageNativeWindow.ISetBitmapError InnerError) : ISetBitmapError; + } + public MorphicResult SetBitmap(System.Drawing.Bitmap? bitmap) + { + if (bitmap is not null) + { + var positionAndResizeBitmapResult = this.PositionAndResizeBitmap(bitmap); + if (positionAndResizeBitmapResult.IsError == true) + { + Debug.Assert(false, "Could not position and resize bitmap."); + var innerError = positionAndResizeBitmapResult.Error!; + return MorphicResult.ErrorResult(new ISetBitmapError.CouldNotPositionAndResizeBitmap(innerError)); + } + } + + if (_argbImageNativeWindow is not null) + { + var setBitmapOnArgbImageNativeWindowResult = _argbImageNativeWindow!.SetBitmap(bitmap); + if (setBitmapOnArgbImageNativeWindowResult.IsError == true) + { + Debug.Assert(false, "Could not set bitmap on ARGB image native window."); + var innerError = setBitmapOnArgbImageNativeWindowResult.Error!; + return MorphicResult.ErrorResult(new ISetBitmapError.CouldNotSetBitmapInArgbImageNativeWindow(innerError)); + } + } + + return MorphicResult.OkResult(); + } + + internal interface IPositionAndResizeBitmapError + { + public record CouldNotGetCurrentPositionAndSize(uint Win32ErrorCode) : IPositionAndResizeBitmapError; + public record CouldNotSetNewPositionAndSize(ArgbImageNativeWindow.ISetPositionAndSizeError InnerError) : IPositionAndResizeBitmapError; + } + // + private MorphicResult PositionAndResizeBitmap(System.Drawing.Bitmap bitmap) + { + // then, reposition the bitmap + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowrect + Windows.Win32.Foundation.RECT positionAndSize; + var getWindowRectResult = Windows.Win32.PInvoke.GetWindowRect((Windows.Win32.Foundation.HWND)this.Handle, out positionAndSize); + if (getWindowRectResult == 0) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new IPositionAndResizeBitmapError.CouldNotGetCurrentPositionAndSize((uint)win32ErrorCode)); + } + // + var bitmapSize = bitmap.Size; + + var argbImageNativeWindowSize = TrayButtonNativeWindow.CalculateWidthAndHeightForBitmap(positionAndSize, bitmapSize); + var bitmapRect = TrayButtonNativeWindow.CalculateCenterRectInsideRect(positionAndSize, argbImageNativeWindowSize); + + if (_argbImageNativeWindow is not null) + { + var setPositionAndSizeResult = _argbImageNativeWindow!.SetPositionAndSize(bitmapRect); + if (setPositionAndSizeResult.IsError == true) + { + var innerError = setPositionAndSizeResult.Error!; + return MorphicResult.ErrorResult(new IPositionAndResizeBitmapError.CouldNotSetNewPositionAndSize(innerError)); + } + } + + return MorphicResult.OkResult(); + } + + public MorphicResult SetText(string? text) + { + _tooltipText = text; + + var updateTooltipTextAndTrackingResult = this.UpdateTooltipTextAndTracking(); + if (updateTooltipTextAndTrackingResult.IsError == true) + { + // NOTE: we simply pass through this error + Debug.Assert(false, "Could not update tooltip text"); + return MorphicResult.ErrorResult(updateTooltipTextAndTrackingResult.Error!); + } + + return MorphicResult.OkResult(); + } + + // + + private IntPtr CreateTooltipWindow() + { + if (_tooltipWindowHandle != IntPtr.Zero) + { + // tooltip window already exists; gracefully degrate by returning the existing window handle + return _tooltipWindowHandle; + } + + Windows.Win32.Foundation.HWND tooltipWindowHandle; + unsafe + { + tooltipWindowHandle = Windows.Win32.PInvoke.CreateWindowEx( + 0 /* no styles */, + PInvokeExtensions.TOOLTIPS_CLASS, + null, + Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE.WS_POPUP | (Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE)Windows.Win32.PInvoke.TTS_ALWAYSTIP, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + PInvokeExtensions.CW_USEDEFAULT, + (Windows.Win32.Foundation.HWND)this.Handle, + null, + null, + null); + } + + // NOTE: Microsoft's documentation seems to indicate that we should set the tooltip as topmost, but in our testing this was unnecessary. It's possible that using SendMessage to add/remove tooltip text automatically handles this when the system handles showing the tooltip + // see: https://learn.microsoft.com/en-us/windows/win32/controls/tooltip-controls + //PInvoke.User32.SetWindowPos(tooltipWindowHandle, PInvokeExtensions.HWND_TOPMOST, 0, 0, 0, 0, PInvoke.User32.SetWindowPosFlags.SWP_NOMOVE | PInvoke.User32.SetWindowPosFlags.SWP_NOSIZE | PInvoke.User32.SetWindowPosFlags.SWP_NOACTIVATE); + + Debug.Assert(tooltipWindowHandle.IsNull == false, "Could not create tooltip window."); + + return tooltipWindowHandle; + } + + private MorphicResult DestroyTooltipWindow() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + return MorphicResult.OkResult(); + } + + // set the tooltip text to empty (so that UpdateTooltipText will clear out the tooltip), then update the tooltip text. + _tooltipText = null; + _ = this.UpdateTooltipTextAndTracking(); + + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow + var destroyWindowResult = Windows.Win32.PInvoke.DestroyWindow((Windows.Win32.Foundation.HWND)_tooltipWindowHandle); + _tooltipWindowHandle = (Windows.Win32.Foundation.HWND)IntPtr.Zero; + + if (destroyWindowResult == true) + { + return MorphicResult.OkResult(); + } + else + { + return MorphicResult.ErrorResult(); + } + } + + internal interface IUpdateTooltipTextAndTrackingError + { + public record CouldNotGetTrayButtonClientRect(uint Win32ErrorCode) : IUpdateTooltipTextAndTrackingError; + public record CouldNotUpdateTooltipViaSendMessage : IUpdateTooltipTextAndTrackingError; + public record TooltipWindowDoesNotExist : IUpdateTooltipTextAndTrackingError; + public record TrayButtonWindowDoesNotExist : IUpdateTooltipTextAndTrackingError; + } + private MorphicResult UpdateTooltipTextAndTracking() + { + if (_tooltipWindowHandle == IntPtr.Zero) + { + // tooltip window does not exist; failed; abort + Debug.Assert(false, "Tooptip window does not exist; if this is an expected failure, remove this assert."); + return MorphicResult.ErrorResult(new IUpdateTooltipTextAndTrackingError.TooltipWindowDoesNotExist()); + } + + var trayButtonNativeWindowHandle = this.Handle; + if (trayButtonNativeWindowHandle == IntPtr.Zero) + { + // tray button window does not exist; there is no tool window to update + return MorphicResult.ErrorResult(new IUpdateTooltipTextAndTrackingError.TrayButtonWindowDoesNotExist()); + } + + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclientrect + var getClientRectSuccess = Windows.Win32.PInvoke.GetClientRect((Windows.Win32.Foundation.HWND)this.Handle, out var trayButtonClientRect); + if (getClientRectSuccess == false) + { + // failed; abort + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + Debug.Assert(false, "Could not get client rect for tray button; could not set up tooltip; win32 errcode: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult(new IUpdateTooltipTextAndTrackingError.CouldNotGetTrayButtonClientRect((uint)win32ErrorCode)); + } + + var toolinfo = new PInvokeExtensions.TOOLINFO(); + toolinfo.cbSize = (uint)Marshal.SizeOf(toolinfo); + toolinfo.hwnd = this.Handle; + toolinfo.uFlags = PInvokeExtensions.TTF_SUBCLASS; + toolinfo.lpszText = _tooltipText; + toolinfo.uId = unchecked((nuint)(nint)this.Handle); // unique identifier (for adding/deleting the tooltip) + toolinfo.rect = trayButtonClientRect; + // + var pointerToToolinfo = Marshal.AllocHGlobal(Marshal.SizeOf(toolinfo)); + try + { + Marshal.StructureToPtr(toolinfo, pointerToToolinfo, false); + if (toolinfo.lpszText is not null) + { + if (_tooltipInfoAdded == false) + { + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagew + // + // see: https://learn.microsoft.com/en-us/windows/win32/controls/ttm-addtool + var addToolSuccess = Windows.Win32.PInvoke.SendMessage((Windows.Win32.Foundation.HWND)_tooltipWindowHandle, Windows.Win32.PInvoke.TTM_ADDTOOL, (Windows.Win32.Foundation.WPARAM)0, pointerToToolinfo); + if (addToolSuccess == 0) + { + Debug.Assert(false, "Could not add tooltip info"); + return MorphicResult.ErrorResult(new IUpdateTooltipTextAndTrackingError.CouldNotUpdateTooltipViaSendMessage()); + } + _tooltipInfoAdded = true; + } + else + { + // delete and re-add the tooltipinfo; this will update all the info (including the text and tracking rect) + // + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagew + // + // see: https://learn.microsoft.com/en-us/windows/win32/controls/ttm-deltool + // NOTE: TTM_DELTOOL does not return a result + _ = Windows.Win32.PInvoke.SendMessage((Windows.Win32.Foundation.HWND)_tooltipWindowHandle, Windows.Win32.PInvoke.TTM_DELTOOL, (Windows.Win32.Foundation.WPARAM)0, pointerToToolinfo); + // + // see: https://learn.microsoft.com/en-us/windows/win32/controls/ttm-addtool + var addToolSuccess = Windows.Win32.PInvoke.SendMessage((Windows.Win32.Foundation.HWND)_tooltipWindowHandle, Windows.Win32.PInvoke.TTM_ADDTOOL, (Windows.Win32.Foundation.WPARAM)0, pointerToToolinfo); + if (addToolSuccess == 0) + { + Debug.Assert(false, "Could not update tooltip info"); + return MorphicResult.ErrorResult(new IUpdateTooltipTextAndTrackingError.CouldNotUpdateTooltipViaSendMessage()); + } + } + } + else /* if (_tooltipInfoAdded == true) */ + { + // NOTE: we might technically call "deltool" even when a tooltipinfo was already removed + // + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagew + // + // see: https://learn.microsoft.com/en-us/windows/win32/controls/ttm-deltool + _ = Windows.Win32.PInvoke.SendMessage((Windows.Win32.Foundation.HWND)_tooltipWindowHandle, Windows.Win32.PInvoke.TTM_DELTOOL, (Windows.Win32.Foundation.WPARAM)0, pointerToToolinfo); + _tooltipInfoAdded = false; + } + } + finally + { + Marshal.FreeHGlobal(pointerToToolinfo); + } + + return MorphicResult.OkResult(); + } + + // + + /* helper functions */ + + internal static Windows.Win32.Foundation.RECT CalculateCenterRectInsideRect(Windows.Win32.Foundation.RECT outerRect, System.Drawing.Size innerSize) + { + var outerWidth = outerRect.right - outerRect.left; + var outerHeight = outerRect.bottom - outerRect.top; + + var innerWidth = Math.Min(innerSize.Width, outerWidth); + var innerHeight = Math.Min(innerSize.Height, outerHeight); + + var left = outerRect.left + ((outerWidth - innerWidth) / 2); + var top = outerRect.top + ((outerHeight - innerHeight) / 2); + var right = left + innerWidth; + var bottom = top + innerHeight; + + return new Windows.Win32.Foundation.RECT() + { + left = left, + top = top, + right = right, + bottom = bottom, + }; + } + + internal interface ICalculatePositionAndSizeForTrayButtonError + { + public record CouldNotFindTaskbarRelatedHandle : ICalculatePositionAndSizeForTrayButtonError; + public record CannotFitOnTaskbar : ICalculatePositionAndSizeForTrayButtonError; + public record Win32Error(uint Win32ErrorCode) : ICalculatePositionAndSizeForTrayButtonError; + } + internal static MorphicResult CalculatePositionAndSizeForTrayButton(IntPtr? trayButtonHandle) + { + // NOTE: in this implementation, we simply place the tray button over the taskbar, directly to the left of the system tray + // in the future, we may want to consider searching for any children which might occupy the area--and any system windows which are owned by the taskbar or any of its children--and then try to find a place to the "left" of those + + // get the handles for the taskbar, task button container, and the notify tray + // + var getTaskbarHandleResult = TrayButtonNativeWindow.GetWindowsTaskbarHandle(); + if (getTaskbarHandleResult.IsError == true) + { + switch (getTaskbarHandleResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var taskbarHandle = getTaskbarHandleResult.Value!; + if (taskbarHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CouldNotFindTaskbarRelatedHandle()); + } + // + var getTaskButtonContainerHandleResult = TrayButtonNativeWindow.GetWindowsTaskbarTaskButtonContainerHandle(taskbarHandle); + if (getTaskButtonContainerHandleResult.IsError == true) + { + switch (getTaskButtonContainerHandleResult.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var taskButtonContainerHandle = getTaskButtonContainerHandleResult.Value!; + if (taskButtonContainerHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CouldNotFindTaskbarRelatedHandle()); + } + // + var getNotifyTrayHandle = TrayButtonNativeWindow.GetWindowsTaskbarNotificationTrayHandle(taskbarHandle); + if (getNotifyTrayHandle.IsError == true) + { + switch (getNotifyTrayHandle.Error!) + { + case Morphic.WindowsNative.IWin32ApiError.Win32Error(var win32ErrorCode): + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error(win32ErrorCode)); + default: + throw new MorphicUnhandledErrorException(); + } + } + var notifyTrayHandle = getNotifyTrayHandle.Value!; + if (notifyTrayHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CouldNotFindTaskbarRelatedHandle()); + } + + // get the RECTs for the taskbar, task button container and the notify tray + // + var getTaskbarRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskbarHandle, out var taskbarRect); + if (getTaskbarRectSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error((uint)win32ErrorCode)); + } + // + var getTaskButtonContainerRectSuccess = Windows.Win32.PInvoke.GetWindowRect(taskButtonContainerHandle, out var taskButtonContainerRect); + if (getTaskButtonContainerRectSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error((uint)win32ErrorCode)); + } + // + var getNotifyTrayRectSuccess = Windows.Win32.PInvoke.GetWindowRect(notifyTrayHandle, out var notifyTrayRect); + if (getNotifyTrayRectSuccess == false) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.Win32Error((uint)win32ErrorCode)); + } + + // determine the taskbar's orientation + // + System.Windows.Forms.Orientation taskbarOrientation; + if ((taskbarRect.right - taskbarRect.left) > (taskbarRect.bottom - taskbarRect.top)) + { + taskbarOrientation = System.Windows.Forms.Orientation.Horizontal; + } + else + { + taskbarOrientation = System.Windows.Forms.Orientation.Vertical; + } + + // if the taskbar is horizontal, determine if it's LeftToRight (standard) or RightToLeft (for Arabic, Hebrew, etc.) + bool isRightToLeft = false; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + var centerXOfTaskbar = taskbarRect.X + (taskbarRect.Width / 2); + if (notifyTrayRect.right < centerXOfTaskbar) + { + isRightToLeft = true; + } + } + + // establish the appropriate size for our tray button (i.e. same height/width as taskbar, and with an aspect ratio of 8:10) + int trayButtonHeight; + int trayButtonWidth; + // NOTE: on some computers, the taskbar and notify tray return an inaccurate size, but the task button container appears to always return the correct size; therefore we match our primary dimension to the taskbutton container's same dimension + // NOTE: the inaccurate size returned by GetWindowRect may be due to our moving this class from the main application to a helper library (i.e. perhaps the pixel scaling isn't applying correctly), or it could just be a weird quirk on some computers. + // [The GetWindowRect issue happens with both our own homebuilt PINVOKE methods as well as with PInvoke.User32.GetWindowRect; the function is returning the correct left, bottom and right positions of the taskbar and notify tray--but is + // sometimes misrepresenting the top (i.e. height) value of both the taskbar and notify tray rects] + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + // option 1: base our primary dimension off of the taskbutton container's same dimension + trayButtonHeight = taskButtonContainerRect.bottom - taskButtonContainerRect.top; + // + // option 2: base our primary dimension off of the taskbar's same dimension + //trayButtonHeight = taskbarRect.bottom - taskbarRect.top; + // + // [and then scale the secondary dimension to 80% of the size of the primary dimension] + trayButtonWidth = (int)((Double)trayButtonHeight * 0.8); + } + else + { + // option 1: base our primary dimension off of the taskbutton container's same dimension + trayButtonWidth = taskButtonContainerRect.right - taskButtonContainerRect.left; + // + // option 2: base our primary dimension off of the taskbar's same dimension + //trayButtonWidth = taskbarRect.right - taskbarRect.left; + // + // [and then scale the secondary dimension to 80% of the size of the primary dimension] + trayButtonHeight = (int)((Double)trayButtonWidth * 0.8); + } + + // choose a space in the rightmost/bottommost position of the taskbar; note that "rightmost" is actually leftmost when the system is using an RTL orientation (e.g. Arabic, Hebrew) + int trayButtonX; + int trayButtonY; + if (taskbarOrientation == System.Windows.Forms.Orientation.Horizontal) + { + if (isRightToLeft == false) + { + trayButtonX = notifyTrayRect.left - trayButtonWidth; + if (trayButtonX - trayButtonWidth < taskbarRect.left) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CannotFitOnTaskbar()); + } + } + else + { + trayButtonX = notifyTrayRect.right; + if (trayButtonX + trayButtonWidth > taskbarRect.right) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CannotFitOnTaskbar()); + } + } + // + // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.bottom (if we chose option #1 for our size calculations above) + trayButtonY = taskbarRect.bottom - trayButtonHeight; + } + else /* if (taskbarOrientation == System.Windows.Forms.Orientation.Vertical) */ + { + // NOTE: if we have any issues with positioning, try to replace taskbarRect.bottom with taskButtoncontainerRect.right (if we chose option #1 for our size calculations above) + trayButtonX = taskbarRect.right - trayButtonWidth; + // + trayButtonY = notifyTrayRect.top - trayButtonHeight; + if (trayButtonY - trayButtonHeight < taskbarRect.top) + { + return MorphicResult.ErrorResult(new ICalculatePositionAndSizeForTrayButtonError.CannotFitOnTaskbar()); + } + } + + var result = new Windows.Win32.Foundation.RECT() { left = trayButtonX, top = trayButtonY, right = trayButtonX + trayButtonWidth, bottom = trayButtonY + trayButtonHeight }; + return MorphicResult.OkResult(result); + } + + // + + private MorphicResult ConvertMouseMessageLParamToScreenPoint(IntPtr lParam) + { + var x = (ushort)((lParam.ToInt64() >> 0) & 0xFFFF); + var y = (ushort)((lParam.ToInt64() >> 16) & 0xFFFF); + // convert x and y to screen coordinates + var hitPoint = new PInvoke.POINT { x = x, y = y }; + + // NOTE: the instructions for MapWindowPoints instruct us to call SetLastError before calling MapWindowPoints to ensure that we can distinguish a result of 0 from an error if the last win32 error wasn't set (because it wasn't an error) + // see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapwindowpoints + System.Runtime.InteropServices.Marshal.SetLastPInvokeError(0); + // + // NOTE: the PInvoke implementation of MapWindowPoints did not support passing in a POINT struct, so we manually declared the function + var mapWindowPointsResult = PInvokeExtensions.MapWindowPoints(this.Handle, IntPtr.Zero, ref hitPoint, 1); + if (mapWindowPointsResult == 0) + { + // failed (if the last error != 0) + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + if (win32ErrorCode != 0) + { + Debug.Assert(false, "Could not map tray button hit point to screen coordinates; win32 errcode: " + win32ErrorCode.ToString()); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32ErrorCode)); + } + } + + var result = new System.Drawing.Point(hitPoint.x, hitPoint.y); + return MorphicResult.OkResult(result); + } + + // + + // NOTE: this function takes the window size as input and calculates the size of the icon to display, centered, within the window. + private static System.Drawing.Size CalculateWidthAndHeightForBitmap(Windows.Win32.Foundation.RECT availableRect, System.Drawing.Size bitmapSize) + { + var availableSize = new System.Drawing.Size(availableRect.right - availableRect.left, availableRect.bottom - availableRect.top); + + /* determine the larger dimension (width or height) */ + //int largerDimensionSize; + //int smallerDimensionSize; + System.Drawing.Size insideMarginsSize; + if (availableSize.Height > availableSize.Width) + { + //largerDimensionSize = availableSize.Height; + //smallerDimensionSize = availableSize.Width; + // + // strategy 1: consume up to 90% of the width of the box and up to 66% of the height + //insideMarginsSize = new((int)((double)availableSize.Width * 0.9), (int)((double)availableSize.Height * (2.0 / 3.0))); + // + // strategy 2: consume up to 66% of the width of the box and up to 66% of the height + insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); + } + else + { + //largerDimensionSize = availableSize.Width; + //smallerDimensionSize = availableSize.Height; + // + // strategy 1: consume up to 66% of the width of the box and up to 90% of the height + //insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * 0.9)); + // + // strategy 2: consume up to 66% of the width of the box and up to 66% of the height + insideMarginsSize = new((int)((double)availableSize.Width * (2.0 / 3.0)), (int)((double)availableSize.Height * (2.0 / 3.0))); + } + + /* shrink the bitmap size down so that it fits inside the available rect */ + + // by default, assume the bitmap will be the size of the source image + int bitmapWidth = bitmapSize.Width; + int bitmapHeight = bitmapSize.Height; + // + // if bitmap is wider than the available rect, shrink it equally in both directions + if (bitmapWidth > insideMarginsSize.Width) + { + double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; + bitmapWidth = insideMarginsSize.Width; + bitmapHeight = (int)((double)bitmapHeight * scaleFactor); + } + // + // if bitmap is taller than the available rect, shrink it further (and equally in both directions) + if (bitmapHeight > insideMarginsSize.Height) + { + double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; + bitmapWidth = (int)((double)bitmapWidth * scaleFactor); + bitmapHeight = insideMarginsSize.Height; + } + + // if bitmap does not touch either of the two margins (i.e. is too small), enlarge it now. + if (bitmapWidth != insideMarginsSize.Width && bitmapHeight != insideMarginsSize.Height) + { + // if bitmap is not as wide as the insideMarginsWidth, enlarge it now (equally in both directions) + if (bitmapWidth < insideMarginsSize.Width) + { + double scaleFactor = (double)insideMarginsSize.Width / (double)bitmapWidth; + bitmapWidth = insideMarginsSize.Width; + bitmapHeight = (int)((double)bitmapHeight * scaleFactor); + } + // + // if bitmap is now too tall, shrink it back down (equally in both directions) + if (bitmapHeight > insideMarginsSize.Height) + { + double scaleFactor = (double)insideMarginsSize.Height / (double)bitmapHeight; + bitmapWidth = (int)((double)bitmapWidth * scaleFactor); + bitmapHeight = insideMarginsSize.Height; + } + } + + return new System.Drawing.Size(bitmapWidth, bitmapHeight); + } + + // + + private static MorphicResult GetWindowsTaskbarHandle() + { + var result = Windows.Win32.PInvoke.FindWindow("Shell_TrayWnd", null); + if (result == Windows.Win32.Foundation.HWND.Null) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32ErrorCode)); + } + + return MorphicResult.OkResult(result); + } + // + private static MorphicResult GetWindowsTaskbarTaskButtonContainerHandle(Windows.Win32.Foundation.HWND taskbarHandle) + { + if (taskbarHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.OkResult(Windows.Win32.Foundation.HWND.Null); + } + + var result = Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, Windows.Win32.Foundation.HWND.Null, "ReBarWindow32", null); + if (result == Windows.Win32.Foundation.HWND.Null) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32ErrorCode)); + } + + return MorphicResult.OkResult(result); + } + // + private static MorphicResult GetWindowsTaskbarNotificationTrayHandle(Windows.Win32.Foundation.HWND taskbarHandle) + { + if (taskbarHandle == Windows.Win32.Foundation.HWND.Null) + { + return MorphicResult.OkResult(Windows.Win32.Foundation.HWND.Null); + } + + var result = Windows.Win32.PInvoke.FindWindowEx(taskbarHandle, Windows.Win32.Foundation.HWND.Null, "TrayNotifyWnd", null); + if (result == Windows.Win32.Foundation.HWND.Null) + { + var win32ErrorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + return MorphicResult.ErrorResult(new Morphic.WindowsNative.IWin32ApiError.Win32Error((uint)win32ErrorCode)); + } + + return MorphicResult.OkResult(result); + } +} diff --git a/Morphic.Core.Tests/JsonExtensionTests.cs b/Morphic.Core.Tests/JsonExtensionTests.cs deleted file mode 100644 index a020c4c9..00000000 --- a/Morphic.Core.Tests/JsonExtensionTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using Xunit; -using System.Collections.Generic; -using System.Text.Json; - -namespace Morphic.Core.Tests -{ - public class JsonExtensionTests - { - [Fact] - public void TestBooleans() - { - var json = @"{""a"": true, ""b"": false}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType(o); - Assert.True((bool)o); - Assert.True(result.TryGetValue("b", out o)); - Assert.IsType(o); - Assert.False((bool)o); - } - - [Fact] - public void TestIntegers() - { - var json = @"{""a"": 12, ""b"": 0}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType(o); - Assert.Equal(12, (long)o); - Assert.True(result.TryGetValue("b", out o)); - Assert.IsType(o); - Assert.Equal(0, (long)o); - } - - [Fact] - public void TestFloats() - { - var json = @"{""a"": 12.5, ""b"": 0.1}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType(o); - Assert.True(Math.Abs(12.5 - (double)o) < 0.001); - Assert.True(result.TryGetValue("b", out o)); - Assert.IsType(o); - Assert.True(Math.Abs(0.1 - (double)o) < 0.001); - } - - [Fact] - public void TestStrings() - { - var json = @"{""a"": ""hello"", ""b"": """"}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType(o); - Assert.Equal("hello", (string)o); - Assert.True(result.TryGetValue("b", out o)); - Assert.IsType(o); - Assert.Equal("", (string)o); - } - - [Fact] - public void TestNull() - { - var json = @"{""a"": null}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.Null(o); - } - - [Fact] - public void TestArray() - { - var json = @"{""a"": [1, ""two"", null, 4.1, true]}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType(o); - var array = (object[])o; - Assert.Equal(5, array.Length); - - Assert.IsType(array[0]); - Assert.Equal(1, (long)array[0]); - Assert.IsType(array[1]); - Assert.Equal("two", (string)array[1]); - Assert.Null(array[2]); - Assert.IsType(array[3]); - Assert.True(Math.Abs(4.1 - (double)array[3]) < 0.001); - Assert.IsType(array[4]); - Assert.True((bool)array[4]); - } - - [Fact] - public void TestObject() - { - var json = @"{""a"": {""first"": 1, ""second"": ""two""}}"; - var options = new JsonSerializerOptions(); - var converter = new JsonElementInferredTypeConverter(); - options.Converters.Add(converter); - var result = JsonSerializer.Deserialize>(json, options); - object o; - Assert.True(result.TryGetValue("a", out o)); - Assert.IsType>(o); - var dictionary = (Dictionary)o; - Assert.True(dictionary.TryGetValue("first", out o)); - Assert.IsType(o); - Assert.Equal(1, (long)o); - Assert.True(dictionary.TryGetValue("second", out o)); - Assert.IsType(o); - Assert.Equal("two", (string)o); - } - } -} diff --git a/Morphic.Core.Tests/KeychainTests.cs b/Morphic.Core.Tests/KeychainTests.cs deleted file mode 100644 index 60b7a819..00000000 --- a/Morphic.Core.Tests/KeychainTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.IO; -using Xunit; - -namespace Morphic.Core.Tests -{ - public class KeychainTests : IDisposable - { - public KeychainTests() - { - directoryName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - } - - private string directoryName; - - [Fact] - public void TestSaveLoad() - { - var encrypt = new MockEncrypter(); - var options = new KeychainOptions(); - options.Path = Path.Combine(directoryName, "testsave.json"); - var uri = new Uri("http://www.morphic.org"); - var wronguri = new Uri("http://www.gpii.net"); - var username = new UsernameCredentials("passuser", "password"); - var key = new KeyCredentials("key"); - var logger = new LoggerFactory().CreateLogger(); - //TEST SAVING - var keychain = new Keychain(options, encrypt, logger); - Assert.True(keychain.Save(username, uri)); - Assert.Equal(1, encrypt.encryptCounter); - Assert.Equal(0, encrypt.decryptCounter); - Assert.True(keychain.Save(key, uri, "keyuser")); - Assert.Equal(2, encrypt.encryptCounter); - Assert.Equal(0, encrypt.decryptCounter); - //TEST LOADING - keychain = new Keychain(options, encrypt, logger); - Assert.Equal(2, encrypt.encryptCounter); - Assert.Equal(1, encrypt.decryptCounter); - //TEST RETRIEVAL - var newusername = keychain.LoadUsername(uri, "passuser"); - Assert.Equal("passuser", newusername.Username); - Assert.Equal("password", newusername.Password); - var newkey = keychain.LoadKey(uri, "keyuser"); - Assert.Equal("key", newkey.Key); - newusername = keychain.LoadUsername(uri, "notathing"); - newkey = keychain.LoadKey(uri, "notathing"); - Assert.Null(newusername); - Assert.Null(newkey); - //TODO: put any tests for switching usernames and keys here once that does something - newusername = keychain.LoadUsername(wronguri, "passuser"); - newkey = keychain.LoadKey(wronguri, "keyuser"); - Assert.Null(newusername); - Assert.Null(newkey); - } - - class MockEncrypter : IDataProtection - { - public int encryptCounter = 0; - public int decryptCounter = 0; - public byte[] Protect(byte[] userData) - { - ++encryptCounter; - return userData; - } - - public byte[] Unprotect(byte[] encryptedData) - { - ++decryptCounter; - return encryptedData; - } - } - - public void Dispose() - { - Directory.Delete(directoryName, true); - } - } -} diff --git a/Morphic.Core.Tests/Morphic.Core.Tests.csproj b/Morphic.Core.Tests/Morphic.Core.Tests.csproj deleted file mode 100644 index 0f6d17ff..00000000 --- a/Morphic.Core.Tests/Morphic.Core.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netcoreapp3.1 - false - AnyCPU;x64 - 9.0 - enable - - - - - - - - - - - - - - diff --git a/Morphic.Core.Tests/PreferencesTests.cs b/Morphic.Core.Tests/PreferencesTests.cs deleted file mode 100644 index cb604d0b..00000000 --- a/Morphic.Core.Tests/PreferencesTests.cs +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.Text.Json; -using Xunit; - -namespace Morphic.Core.Tests -{ - public class PreferencesTests - { - [Fact] - public void TestJsonDeserialize() - { - //testing fully populated - //TODO: get default serialization working - TestResource tr = new TestResource(); - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonElementInferredTypeConverter()); - var preferencesid = Guid.NewGuid().ToString(); - var userid = Guid.NewGuid().ToString(); - var json = JsonSerializer.Serialize(new Dictionary() - { - { "id", preferencesid }, - { "user_id", userid }, - { "default", tr.Default } - }); - var preferences = JsonSerializer.Deserialize(json, options); - Assert.NotNull(preferences); - Assert.Equal(preferencesid, preferences.Id); - Assert.Equal(userid, preferences.UserId); - Assert.NotNull(preferences.Default); - Assert.Equal("ayy lmao", preferences.Default["firstthing"].Values["thisisastring"]); - Assert.Equal(3.14159d, preferences.Default["firstthing"].Values["thisisadouble"]); - Assert.Equal(52L, preferences.Default["firstthing"].Values["thisisaninteger"]); - Assert.Equal(true, preferences.Default["firstthing"].Values["thisisaboolean"]); - var dictionary = (Dictionary)preferences.Default["firstthing"].Values["thisisadictionary"]; - Assert.Equal(1L, dictionary["one"]); - Assert.Equal(2L, dictionary["two"]); - Assert.Equal(3L, dictionary["three"]); - Assert.Equal(413L, ((object[])preferences.Default["firstthing"].Values["thisisanarray"])[5]); - - //testing minimally populated - preferencesid = Guid.NewGuid().ToString(); - json = JsonSerializer.Serialize(new Dictionary() - { - { "id", preferencesid } - }); - preferences = JsonSerializer.Deserialize(json, options); - Assert.NotNull(preferences); - Assert.Equal(preferencesid, preferences.Id); - - //TODO: test invalid cases once expected behavior for deserialization failure is known - } - - [Fact] - public void TestJsonSerialize() - { - var preferencesid = Guid.NewGuid().ToString(); - var userid = Guid.NewGuid().ToString(); - var resource = new TestResource(); - var preferences = new Preferences(); - preferences.Id = preferencesid; - preferences.UserId = userid; - preferences.Default = resource.Default; - var json = JsonSerializer.Serialize(preferences); - var jsonobject = JsonDocument.Parse(json).RootElement; - Assert.Equal(preferencesid, jsonobject.GetProperty("id").GetString()); - Assert.Equal(userid, jsonobject.GetProperty("user_id").GetString()); - Assert.Equal("ayy lmao", jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisastring").GetString()); - Assert.Equal(3.14159d, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisadouble").GetDouble()); - Assert.Equal(52L, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisaninteger").GetInt64()); - Assert.True(jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisaboolean").GetBoolean()); - Assert.Equal(1L, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisadictionary").GetProperty("one").GetInt64()); - Assert.Equal(2L, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisadictionary").GetProperty("two").GetInt64()); - Assert.Equal(3L, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisadictionary").GetProperty("three").GetInt64()); - Assert.Equal(413L, jsonobject.GetProperty("default").GetProperty("firstthing").GetProperty("thisisanarray")[5].GetInt64()); - } - - [Fact] - public void TestGet() - { - var preferences = new Preferences(); - preferences.Default = new TestResource().Default; - //fetch every data type - var returnstring = preferences.Get(new Preferences.Key("firstthing", "thisisastring")); - var returndouble = preferences.Get(new Preferences.Key("firstthing", "thisisadouble")); - var returnint = preferences.Get(new Preferences.Key("firstthing", "thisisaninteger")); - var returnboolean = preferences.Get(new Preferences.Key("firstthing", "thisisaboolean")); - var returndictionary = preferences.Get(new Preferences.Key("firstthing", "thisisadictionary")); - var returnarray = preferences.Get(new Preferences.Key("firstthing", "thisisanarray")); - //try to fetch something that isn't there, and something in a different solution - var nothere = preferences.Get(new Preferences.Key("firstthing", "somethingdifferent")); - var wrongplace = preferences.Get(new Preferences.Key("secondthing", "thisisadictionary")); - - Assert.IsType(returnstring); - Assert.Equal(returnstring, preferences.Default["firstthing"].Values["thisisastring"]); - Assert.IsType(returndouble); - Assert.Equal(returndouble, preferences.Default["firstthing"].Values["thisisadouble"]); - Assert.IsType(returnint); - Assert.Equal(returnint, preferences.Default["firstthing"].Values["thisisaninteger"]); - Assert.IsType(returnboolean); - Assert.Equal(returnboolean, preferences.Default["firstthing"].Values["thisisaboolean"]); - Assert.IsType>(returndictionary); - Assert.Equal(returndictionary, preferences.Default["firstthing"].Values["thisisadictionary"]); - Assert.IsType(returnarray); - Assert.Equal(returnarray, preferences.Default["firstthing"].Values["thisisanarray"]); - Assert.Null(nothere); - Assert.Null(wrongplace); - } - - [Fact] - public void TestSet() - { - var preferences = new Preferences(); - var resource = new TestResource(); - //prefs.Default = new tResource1().Default; - preferences.Set(new Preferences.Key("firstthing", "thisisastring"), "set the string with a different value to start"); - preferences.Set(new Preferences.Key("firstthing", "thisisadouble"), 3.14159d); - preferences.Set(new Preferences.Key("firstthing", "thisisaninteger"), "whoops I used the wrong data type"); - preferences.Set(new Preferences.Key("firstthing", "thisisaninteger"), 12345L); - preferences.Set(new Preferences.Key("firstthing", "thisisaboolean"), true); - preferences.Set(new Preferences.Key("firstthing", "thisisadictionary"), 823847L); - preferences.Set(new Preferences.Key("firstthing", "thisisadictionary"), resource.Default["firstthing"].Values["thisisadictionary"]); - preferences.Set(new Preferences.Key("firstthing", "thisisanarray"), new object[40]); - preferences.Set(new Preferences.Key("firstthing", "thisisastring"), "now change the string"); - - Assert.IsType(preferences.Default["firstthing"].Values["thisisastring"]); - Assert.Equal("now change the string", preferences.Default["firstthing"].Values["thisisastring"]); - Assert.IsType(preferences.Default["firstthing"].Values["thisisadouble"]); - Assert.Equal(3.14159d, preferences.Default["firstthing"].Values["thisisadouble"]); - Assert.IsType(preferences.Default["firstthing"].Values["thisisaninteger"]); - Assert.Equal(12345L, preferences.Default["firstthing"].Values["thisisaninteger"]); - Assert.IsType(preferences.Default["firstthing"].Values["thisisaboolean"]); - Assert.Equal(true, preferences.Default["firstthing"].Values["thisisaboolean"]); - Assert.IsType>(preferences.Default["firstthing"].Values["thisisadictionary"]); - Assert.Equal(resource.Default["firstthing"].Values["thisisadictionary"], preferences.Default["firstthing"].Values["thisisadictionary"]); - Assert.IsType(preferences.Default["firstthing"].Values["thisisanarray"]); - Assert.Equal(new object[40], preferences.Default["firstthing"].Values["thisisanarray"]); - } - - [Fact] - public void TestCopyConstructor() - { - var preferences = new Preferences(); - var key1 = new Preferences.Key("org.raisingthefloor.test", "one"); - var key2 = new Preferences.Key("org.raisingthefloor.test", "two"); - var key3 = new Preferences.Key("org.raisingthefloor.test", "three"); - preferences.Set(key1, "Hello"); - preferences.Set(key2, 12L); - preferences.Set(key3, new Dictionary() - { - { "a", "value1" }, - { "b", "value2" } - }); - - var preferences2 = new Preferences(preferences); - - var value = preferences2.Get(key1); - Assert.IsType(value); - Assert.Equal("Hello", (string)value); - value = preferences2.Get(key2); - Assert.IsType(value); - Assert.Equal(12, (long)value); - value = preferences2.Get(key3); - Assert.IsType>(value); - Assert.Equal("value1", ((Dictionary)value)["a"]); - Assert.Equal("value2", ((Dictionary)value)["b"]); - - preferences.Set(key1, "World"); - value = preferences2.Get(key1); - Assert.IsType(value); - Assert.Equal("Hello", (string)value); - - preferences2.Set(key1, "Testing"); - value = preferences.Get(key1); - Assert.IsType(value); - Assert.Equal("World", (string)value); - - preferences.Set(key3, new Dictionary() - { - { "a", "changed1" } - }); - - value = preferences2.Get(key3); - Assert.IsType>(value); - Assert.Equal("value1", ((Dictionary)value)["a"]); - Assert.Equal("value2", ((Dictionary)value)["b"]); - - preferences2.Set(key3, new Dictionary() - { - { "a", "changed2" } - }); - - value = preferences.Get(key3); - Assert.IsType>(value); - Assert.Equal("changed1", ((Dictionary)value)["a"]); - } - - [Fact] - void TestRemove() - { - var preferences = new Preferences(); - var key1 = new Preferences.Key("org.raisingthefloor.test", "one"); - var key2 = new Preferences.Key("org.raisingthefloor.test", "two"); - var key3 = new Preferences.Key("org.raisingthefloor.test2", "three"); - preferences.Set(key1, "Hello"); - preferences.Set(key2, 12L); - preferences.Set(key3, new Dictionary() - { - { "a", "value1" }, - { "b", "value2" } - }); - - Assert.True(preferences.Default.TryGetValue("org.raisingthefloor.test", out var solutionPreferences)); - Assert.True(solutionPreferences.Values.ContainsKey("one")); - Assert.True(solutionPreferences.Values.ContainsKey("two")); - Assert.True(preferences.Default.TryGetValue("org.raisingthefloor.test2", out solutionPreferences)); - Assert.True(solutionPreferences.Values.ContainsKey("three")); - - preferences.Remove(key1); - - Assert.True(preferences.Default.TryGetValue("org.raisingthefloor.test", out solutionPreferences)); - Assert.False(solutionPreferences.Values.ContainsKey("one")); - Assert.True(solutionPreferences.Values.ContainsKey("two")); - Assert.True(preferences.Default.TryGetValue("org.raisingthefloor.test2", out solutionPreferences)); - Assert.True(solutionPreferences.Values.ContainsKey("three")); - - preferences.Remove(key3); - - Assert.True(preferences.Default.TryGetValue("org.raisingthefloor.test", out solutionPreferences)); - Assert.False(solutionPreferences.Values.ContainsKey("one")); - Assert.True(solutionPreferences.Values.ContainsKey("two")); - Assert.False(preferences.Default.TryGetValue("org.raisingthefloor.test2", out solutionPreferences)); - - preferences.Remove(key2); - - Assert.False(preferences.Default.TryGetValue("org.raisingthefloor.test", out solutionPreferences)); - Assert.False(preferences.Default.TryGetValue("org.raisingthefloor.test2", out solutionPreferences)); - } - - //test resources - - class TestResource - { - public Dictionary Default; - public string serialized; - public TestResource() - { - Default = new Dictionary(); - Default.Add("firstthing", new SolutionPreferences()); - Default["firstthing"].Values.Add("thisisastring", "ayy lmao"); - Default["firstthing"].Values.Add("thisisadouble", 3.14159d); - Default["firstthing"].Values.Add("thisisaninteger", 52L); - Default["firstthing"].Values.Add("thisisaboolean", true); - Dictionary dict = new Dictionary() { { "one", 1L }, { "two", 2L }, { "three", 3L } }; - Default["firstthing"].Values.Add("thisisadictionary", dict); - object[] arr = new object[10]; - arr[5] = 413L; - Default["firstthing"].Values.Add("thisisanarray", arr); - Default.Add("secondthing", new SolutionPreferences()); - Default["secondthing"].Values.Add("thisisaboolean", false); - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonElementInferredTypeConverter()); - serialized = JsonSerializer.Serialize(Default, options); - } - } - } -} \ No newline at end of file diff --git a/Morphic.Core.Tests/StorageTests.cs b/Morphic.Core.Tests/StorageTests.cs deleted file mode 100644 index 35047703..00000000 --- a/Morphic.Core.Tests/StorageTests.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json.Serialization; -using Xunit; - -namespace Morphic.Core.Tests -{ - public class StorageTests : IDisposable - { - public StorageTests() - { - directoryName = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - byte[] junk = new byte[100]; - Random rnd = new Random(); - rnd.NextBytes(junk); - Directory.CreateDirectory(Path.Combine(directoryName, "MockRecord")); - File.WriteAllBytes(Path.Combine(directoryName, "MockRecord/binaryfile.json"), junk); - File.WriteAllText(Path.Combine(directoryName, "MockRecord/badjsonfile.json"), "{\"whoops\"; \"thisisafail\"}"); - File.WriteAllText(Path.Combine(directoryName, "MockRecord/incorrectfields.json"), "{\"notarecord\": \"atall\"}"); - File.WriteAllText(Path.Combine(directoryName, "MockRecord/notajsonfile"), "{\"ayy\":\"lmao\"}"); - } - - - private string directoryName; - - [Fact] - public async void TestSaveLoad() - { - var options = new StorageOptions(); - options.RootPath = directoryName; - var logger = new LoggerFactory().CreateLogger(); - //SAVING TEST - Storage storage = new Storage(options, logger); - var mock = new MockRecord(); - mock.populate(); - bool sav = (await storage.SaveAsync(mock)).IsSuccess; - Assert.True(sav); - //EXISTS TEST - Assert.True(storage.Exists("testrecord")); - Assert.True(storage.Exists("binaryfile")); - Assert.True(storage.Exists("badjsonfile")); - Assert.True(storage.Exists("incorrectfields")); - Assert.False(storage.Exists("aintherechief")); - Assert.False(storage.Exists("testrecord")); - Assert.False(storage.Exists("notajsonfile")); - //LOAD TEST - var testfile = await storage.LoadAsync("testrecord"); - var nofile = await storage.LoadAsync("thisfileisnthere"); - var wrongfields = await storage.LoadAsync("incorrectfields"); - var badjsonfile = await storage.LoadAsync("badjsonfile"); - var notajson = await storage.LoadAsync("notajsonfile"); - var binaryfile = await storage.LoadAsync("binaryfile"); - Assert.NotNull(testfile); - Assert.Equal(mock.UserId, testfile.UserId); - Assert.Equal(mock.PreferencesId, testfile.PreferencesId); - Assert.Equal(mock.FirstName, testfile.FirstName); - Assert.Equal(mock.LastName, testfile.LastName); - Assert.Equal(mock.Default["firstthing"].Values["thisisastring"], testfile.Default["firstthing"].Values["thisisastring"]); - Assert.Equal(mock.Default["firstthing"].Values["thisisadouble"], testfile.Default["firstthing"].Values["thisisadouble"]); - Assert.Equal(mock.Default["firstthing"].Values["thisisaninteger"], testfile.Default["firstthing"].Values["thisisaninteger"]); - Assert.Equal(mock.Default["firstthing"].Values["thisisaboolean"], testfile.Default["firstthing"].Values["thisisaboolean"]); - Assert.Null(nofile); - //Assert.Null(wrongfields); //TODO: assess whether check should be done as to whether proper fields are provided? - Assert.Null(badjsonfile); - Assert.Null(notajson); - Assert.Null(binaryfile); - } - - private string GetTestPath(string file) - { - return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..\\..\\..\\testfiles\\storage", file)); - } - - #nullable enable - - //record class that contains all fields to be tested - class MockRecord : IRecord - { - [JsonPropertyName("id")] - string IRecord.Id { get; set; } = "testrecord"; - - [JsonPropertyName("user_id")] - public string? UserId { get; set; } - - [JsonPropertyName("preferences_id")] - public string? PreferencesId { get; set; } - - [JsonPropertyName("first_name")] - public string? FirstName { get; set; } - - [JsonPropertyName("last_name")] - public string? LastName { get; set; } - - [JsonPropertyName("default")] - public Dictionary? Default { get; set; } - - public void populate() - { - UserId = "62398721067952310238967627839"; - PreferencesId = "892319065789120956462348671234"; - FirstName = "John"; - LastName = "Doe"; - Default = new Dictionary(); - Default.Add("firstthing", new SolutionPreferences()); - Default["firstthing"].Values.Add("thisisastring", "ayy lmao"); - Default["firstthing"].Values.Add("thisisadouble", 3.14159d); - Default["firstthing"].Values.Add("thisisaninteger", 52L); - Default["firstthing"].Values.Add("thisisaboolean", true); - Dictionary dict = new Dictionary() { { "one", 1 }, { "two", 2 }, { "three", 3 } }; - Default["firstthing"].Values.Add("thisisadictionary", dict); - object?[] arr = new object?[10]; - Default["firstthing"].Values.Add("thisisanarray", arr); - Default.Add("secondthing", new SolutionPreferences()); - Default["secondthing"].Values.Add("thisisaboolean", false); - } - } - - #nullable disable - - public void Dispose() - { - Directory.Delete(directoryName, true); - } - } -} diff --git a/Morphic.Core.Tests/UserTests.cs b/Morphic.Core.Tests/UserTests.cs deleted file mode 100644 index 7b4fd97a..00000000 --- a/Morphic.Core.Tests/UserTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Text.Json; -using System.Collections.Generic; -using Xunit; - -namespace Morphic.Core.Tests -{ - public class UserTests - { - [Fact] - public void TestJsonDeserialize() - { - // Valid user, all fields populated - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonElementInferredTypeConverter()); - var userid = Guid.NewGuid().ToString(); - var preferencesid = Guid.NewGuid().ToString(); - var json = JsonSerializer.Serialize(new Dictionary() - { - { "id", userid }, - { "preferences_id", preferencesid }, - { "first_name", "John" }, - { "last_name", "Doe" } - }); - var user = JsonSerializer.Deserialize(json, options); - Assert.NotNull(user); - Assert.Equal(userid, user.Id); - Assert.Equal(preferencesid, user.PreferencesId); - Assert.Equal("John", user.FirstName); - Assert.Equal("Doe", user.LastName); - - - // Valid user, minimum set of fields populated - userid = Guid.NewGuid().ToString(); - json = JsonSerializer.Serialize(new Dictionary() - { - { "id", userid } - }); - user = JsonSerializer.Deserialize(json, options); - Assert.NotNull(user); - Assert.Equal(userid, user.Id); - Assert.Null(user.PreferencesId); - Assert.Null(user.FirstName); - Assert.Null(user.LastName); - - - // Invalid user, all other fields populated - preferencesid = Guid.NewGuid().ToString(); - json = JsonSerializer.Serialize(new Dictionary() - { - { "preferences_id", preferencesid }, - { "first_name", "John" }, - { "last_name", "Doe" } - }); - user = JsonSerializer.Deserialize(json, options); - Assert.NotNull(user); - //TODO: this one should actually fail, need to change the code - } - - [Fact] - public void TestJsonSerialize() - { - var userid = Guid.NewGuid().ToString(); - var preferencesid = Guid.NewGuid().ToString(); - User user = new User(); - user.Id = userid; - user.PreferencesId = preferencesid; - user.FirstName = "John"; - user.LastName = "Doe"; - var json = JsonSerializer.Serialize(user); - var obj = JsonDocument.Parse(json).RootElement; - Assert.Equal(userid, obj.GetProperty("id").GetString()); - Assert.Equal(preferencesid, obj.GetProperty("preferences_id").GetString()); - Assert.Equal("John", obj.GetProperty("first_name").GetString()); - Assert.Equal("Doe", obj.GetProperty("last_name").GetString()); - - user = new User(); - user.Id = userid; - json = JsonSerializer.Serialize(user); - obj = JsonDocument.Parse(json).RootElement; - Assert.Equal(userid, obj.GetProperty("id").GetString()); - Assert.Null(obj.GetProperty("preferences_id").GetObject()); - Assert.Null(obj.GetProperty("first_name").GetObject()); - Assert.Null(obj.GetProperty("last_name").GetObject()); - } - } -} \ No newline at end of file diff --git a/Morphic.Core/Attributes/MorphicStringValueAttribute.cs b/Morphic.Core/Attributes/MorphicStringValueAttribute.cs deleted file mode 100644 index ae01e2f7..00000000 --- a/Morphic.Core/Attributes/MorphicStringValueAttribute.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2021 Raising the Floor - US, Inc. -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-windows/blob/main/LICENSE -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Reflection; - -namespace Morphic.Core -{ - [System.AttributeUsage(AttributeTargets.Field)] - public class MorphicStringValueAttribute : Attribute - { - public string StringValue; - - public MorphicStringValueAttribute(string stringValue) - { - this.StringValue = stringValue; - } - } - - public static partial class MorphicExtensions - { - public static string? ToStringValue(this TEnum value) where TEnum : Enum - { - var memberName = typeof(TEnum).GetEnumName(value); - if (memberName is null) - { - // member does not exist - return null; - } - // - var fieldInfo = typeof(TEnum).GetField(memberName); - // - var attribute = fieldInfo!.GetCustomAttribute(); - if (attribute is null) - { - // this enum member does not have a string value - return null; - } - return attribute.StringValue; - } - } -} \ No newline at end of file diff --git a/Morphic.Core/Community/BarItem.cs b/Morphic.Core/Community/BarItem.cs deleted file mode 100644 index 68eb3186..00000000 --- a/Morphic.Core/Community/BarItem.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Morphic.Core.Community -{ - public class BarItem - { - [JsonPropertyName("kind")] - public BarItemKind Kind { get; set; } - - [JsonPropertyName("is_primary")] - public bool IsPrimary { get; set; } = false; - - [JsonPropertyName("configuration")] - public Dictionary? Configuration { get; set; } - - [JsonIgnore] - public string? ButtonLabel - { - get - { - object? value = null; - if (Configuration?.TryGetValue("label", out value) ?? false) - { - if (value is string stringValue) - { - return stringValue; - } - } - return null; - } - } - - [JsonIgnore] - public Uri? ButtonImageUri - { - get - { - object? value = null; - if (Configuration?.TryGetValue("image_url", out value) ?? false) - { - if (value is string stringValue) - { - try - { - if (stringValue.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || stringValue.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - return new Uri(stringValue); - } - return new Uri(stringValue, UriKind.Relative); - } - catch - { - return null; - } - } - } - return null; - } - } - - [JsonIgnore] - public string? DefaultApplicationName - { - get - { - object? value = null; - if (Configuration?.TryGetValue("default", out value) ?? false) - { - if (value is string stringValue) - { - return stringValue; - } - } - return null; - } - } - - [JsonIgnore] - public string? ExeName - { - get - { - object? value = null; - if (Configuration?.TryGetValue("exe", out value) ?? false) - { - if (value is string stringValue) - { - return stringValue; - } - } - return null; - } - } - - [JsonIgnore] - public string? ActionIdentifier - { - get - { - object? value = null; - if (Configuration?.TryGetValue("identifier", out value) ?? false) - { - if (value is string stringValue) - { - return stringValue; - } - } - return null; - } - } - - [JsonIgnore] - public string? ColorHexString - { - get - { - object? value = null; - if (Configuration?.TryGetValue("color", out value) ?? false) - { - if (value is string stringValue) - { - return stringValue; - } - } - return null; - } - } - - [JsonIgnore] - public Uri? LinkUri - { - get - { - object? value = null; - if (Configuration?.TryGetValue("url", out value) ?? false) - { - if (value is string stringValue) - { - try - { - return new Uri(stringValue); - } - catch - { - return null; - } - } - } - return null; - } - } - } - - public enum BarItemKind - { - Link, - Application, - Action - } -} diff --git a/Morphic.Core/Community/Community.cs b/Morphic.Core/Community/Community.cs deleted file mode 100644 index 72a0a14b..00000000 --- a/Morphic.Core/Community/Community.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System.Text.Json.Serialization; - -namespace Morphic.Core.Community -{ - public class UserCommunity : IRecord - { - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("role")] - public MemberRole Role { get; set; } - } - - public enum MemberRole - { - Member, - Manager - } -} \ No newline at end of file diff --git a/Morphic.Core/Community/UserBar.cs b/Morphic.Core/Community/UserBar.cs deleted file mode 100644 index a60c92c6..00000000 --- a/Morphic.Core/Community/UserBar.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System.Text.Json.Serialization; - -namespace Morphic.Core.Community -{ - public class UserBar : IRecord - { - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("items")] - public BarItem[] Items { get; set; } - } -} diff --git a/Morphic.Core/ICredentials.cs b/Morphic.Core/ICredentials.cs deleted file mode 100644 index b5309881..00000000 --- a/Morphic.Core/ICredentials.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Core -{ - /// - /// Interface to identify credential-holding classes - /// - public interface ICredentials - { - } -} diff --git a/Morphic.Core/IDataProtection.cs b/Morphic.Core/IDataProtection.cs deleted file mode 100644 index 57abfe04..00000000 --- a/Morphic.Core/IDataProtection.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Core -{ - /// - /// An encryption interface for data protection - /// - public interface IDataProtection - { - - /// - /// Encrypt data - /// - /// The data to be encrypted - /// The encrypted data - public byte[] Protect(byte[] userData); - - /// - /// Decrypt data - /// - /// The data to decrypted - /// The unencrypted data - public byte[] Unprotect(byte[] encryptedData); - - } -} diff --git a/Morphic.Core/IRecord.cs b/Morphic.Core/IRecord.cs deleted file mode 100644 index b05580d1..00000000 --- a/Morphic.Core/IRecord.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Morphic.Core -{ - /// - /// A Storable record with a unique identifier - /// - public interface IRecord - { - /// - /// The record's unique identifier - /// - public string Id { get; set; } - } -} diff --git a/Morphic.Core/IUserSettings.cs b/Morphic.Core/IUserSettings.cs deleted file mode 100644 index 124be32b..00000000 --- a/Morphic.Core/IUserSettings.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Morphic.Core -{ - /// - /// A collection of settings that a morphic application manages and persists across app launches - /// - public interface IUserSettings - { - /// - /// The logged in user id - /// - public string? UserId { get; set; } - - /// - /// Get the Username for the given user id - /// - /// - /// - public string? GetUsernameForId(string userId); - - /// - /// Save the given username for the given id - /// - /// - /// - public void SetUsernameForId(string username, string userId); - } -} diff --git a/Morphic.Core/JsonExtensions.cs b/Morphic.Core/JsonExtensions.cs deleted file mode 100644 index dd83ef05..00000000 --- a/Morphic.Core/JsonExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Collections.Generic; - -namespace Morphic.Core -{ - -#nullable disable - - /// Convert JSONElements into standard types like long, string, object[], or Dictionary<string, object> - /// - /// For cases such as Preferences where we allow arbirary JSON and can't indicate the correct types up-front in a class definition. - /// System.Text.Json doesn't do this automatically, so we need to write our own. - /// This implementation simply leverages the JsonElement.GetObject extension found below. - /// - public class JsonElementInferredTypeConverter : JsonConverter - { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var document = JsonDocument.ParseValue(ref reader); - return document.RootElement.GetObject(); - } - - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) - { - throw new InvalidOperationException("For JSON deserilization only"); - } - } - - public static class JsonElementExtensions - { - - /// Return an appropriate primitive type for a Jsonelement - public static object GetObject(this JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Undefined: - return null; - case JsonValueKind.Null: - return null; - case JsonValueKind.True: - return true; - case JsonValueKind.False: - return false; - case JsonValueKind.Number: - if (element.TryGetInt64(out long n)) - { - return n; - } - return element.GetDouble(); - case JsonValueKind.String: - return element.GetString(); - case JsonValueKind.Array: - { - var array = new object[element.GetArrayLength()]; - var i = 0; - foreach (var child in element.EnumerateArray()) - { - array[i++] = child.GetObject(); - } - return array; - } - case JsonValueKind.Object: - { - var dict = new Dictionary(); - foreach (var property in element.EnumerateObject()) - { - dict[property.Name] = property.Value.GetObject(); - } - return dict; - } - default: - throw new Exception(String.Format("Unknown JsonValueKind: {0}", element.ValueKind.ToString())); - } - } - } - -#nullable enable - -} \ No newline at end of file diff --git a/Morphic.Core/KeyCredentials.cs b/Morphic.Core/KeyCredentials.cs deleted file mode 100644 index 7cf144eb..00000000 --- a/Morphic.Core/KeyCredentials.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Security.Cryptography; - -namespace Morphic.Core -{ - - /// - /// Secret key based credentials - /// - public class KeyCredentials : ICredentials - { - - public KeyCredentials() - { - var provider = RandomNumberGenerator.Create(); - var data = new byte[64]; - provider.GetBytes(data); - Key = Convert.ToBase64String(data); - } - - public KeyCredentials(string key) - { - Key = key; - } - - public string Key { get; set; } - } -} diff --git a/Morphic.Core/Keychain.cs b/Morphic.Core/Keychain.cs deleted file mode 100644 index c32fad87..00000000 --- a/Morphic.Core/Keychain.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Text; -using Microsoft.Extensions.Logging; - -namespace Morphic.Core -{ - - /// - /// Options for creating a keychain - /// - /// - /// Designed to pass one or more options to a Keychain constructor via dependency injection - /// - public class KeychainOptions - { - /// - /// The file path of the keychain - /// - public string Path = ""; - } - - /// - /// A keychain that encrypts and saves sensitive user data like passwords - /// - public class Keychain - { - - /// - /// Create a new keychain - /// - /// The creation options - /// A object that can encrypt and decrypt data - /// A logger for the keychain - public Keychain(KeychainOptions options, IDataProtection dataProtection, ILogger logger) - { - path = options.Path; - this.logger = logger; - this.dataProtection = dataProtection; - if (!ReadEncryptedData()) - { - logger.LogError("Failed to read keychain"); - } - } - - /// - /// The keychain's logger - /// - private readonly ILogger logger; - - /// - /// An object that handles the encryption and decryption for the keychain - /// - /// - /// On Windows, the ProtectedData class provides a way to - /// encrypt data for the user, but it's not available in a .net core library, - /// so the keychain delegates its encryption tasks to this object that can - /// be provided by an application with access to ProtectedData - /// - private readonly IDataProtection dataProtection; - - /// - /// Save the key-based credentials to the keychain - /// - /// The credentials to save - /// The endpoint to which they apply - /// The user tied to the credentials - /// - public bool Save(KeyCredentials keyCredentials, Uri endpoint, string userId) - { - var key = userId + ';' + endpoint.ToString(); - values[key] = keyCredentials.Key; - return PersistEncryptedData(); - } - - /// - /// Get the key-based credentials for a given user - /// - /// The endpoint where the credentials are used - /// The identifier of the user tied to the credentials - /// The saved credentials, if found - public KeyCredentials? LoadKey(Uri endpoint, string userId) - { - var key = userId + ';' + endpoint.ToString(); - if (values.TryGetValue(key, out var secretKey)) - { - return new KeyCredentials(secretKey); - } - return null; - } - - /// - /// Save the username/password-based credentials to the keychain - /// - /// The credentials to save - /// The endpoint to which they apply - /// The user tied to the credentials - /// - public bool Save(UsernameCredentials usernameCredentials, Uri endpoint) - { - var key = usernameCredentials.Username + ';' + endpoint.ToString(); - values[key] = usernameCredentials.Password; - return PersistEncryptedData(); - } - - /// - /// Get the username/password-based credentials for a given user - /// - /// The endpoint where the credentials are used - /// The username in the credentials - /// The saved credentials, if found - public UsernameCredentials? LoadUsername(Uri endpoint, string username) - { - var key = username + ';' + endpoint.ToString(); - if (values.TryGetValue(key, out var password)) - { - return new UsernameCredentials(username, password); - } - return null; - } - - private Dictionary values = new Dictionary(); - - private readonly string path; - - private bool ReadEncryptedData() - { - if (!File.Exists(path)) - { - return true; - } - try - { - byte[] encrypted = File.ReadAllBytes(path); - var json = dataProtection.Unprotect(encrypted); - values = JsonSerializer.Deserialize>(json); - return true; - } - catch (Exception e) - { - logger.LogError(e, "Error reading keychain"); - return false; - } - } - - private bool PersistEncryptedData() - { - try - { - if (Path.GetDirectoryName(path) is string parent) - { - if (!Directory.Exists(parent)) - { - Directory.CreateDirectory(parent); - } - } - var json = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(values)); - var encrypted = dataProtection.Protect(json); - File.WriteAllBytes(path, encrypted); - return true; - } - catch (Exception e) - { - logger.LogError(e, "Error writing keychain"); - return false; - } - } - } -} diff --git a/Morphic.Core/Morphic.Core.csproj b/Morphic.Core/Morphic.Core.csproj deleted file mode 100644 index c2c1f1b3..00000000 --- a/Morphic.Core/Morphic.Core.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - netcoreapp3.1 - enable - AnyCPU;x64 - 9.0 - - - - - - - diff --git a/Morphic.Core/MorphicResult.cs b/Morphic.Core/MorphicResult.cs deleted file mode 100644 index c588f3f9..00000000 --- a/Morphic.Core/MorphicResult.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2021 Raising the Floor - US, Inc. -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Morphic.Core -{ - // NOTE: MorphicResult is a construct lovingly borrowed from Rust and other languages; in Morphic we use it extensively to return success or failure from - // function calls; we reserve exceptions for unexpected/unhandled conditions (and generally let those exceptions result in a crashlog). - - // NOTE: we declare both a MorphicResult with two generic types (for the return type of functions) and a non-generic MorphicResult with static factory methods - // which create an Ok or Error response. The non-generic MorphicResult actually creates a boxed value which is then implicitly cast to the MorphicResult - // with generic types. This is a bit of compiler magic to keep code simple while preserving full static typing - - // MorphicResult is the actual result type returned by functions; instances are to be created via the non-generic MorphicResult type (below) - public struct MorphicResult - { - // properties which the caller of a function will check to see if the function succeeded or failed (and the corresponding success/error value) - public TValue? Value { get; internal set; } - public TError? Error { get; internal set; } - public bool IsSuccess { get; internal set; } - public bool IsError { get; internal set; } - - // implicit conversions from MorphicResultOkValue or MorphicResultErrorValue; by doing this, our caller's code doesn't need to specify - // either of the return types when the result is returned to the calling code - - // NOTE: we use separate intermediate types (MorphicResultOkValue vs. MorphicResultErrorValue); this is critical to preserve our ability to have both - // TValue and TError set to the same type (e.g. MorphicUnit). The compiler uses these two separate structs to distinguish success from failure results. - - public static implicit operator MorphicResult(MorphicResultOkValue value) - { - return new MorphicResult() - { - Value = value.Value, - IsSuccess = true, - IsError = false - }; - } - - public static implicit operator MorphicResult(MorphicResultErrorValue error) - { - return new MorphicResult() - { - Error = error.Error, - IsSuccess = false, - IsError = true - }; - } - } - - // MorphicResult is is the non-generic helper type used when functions want to return a result - // NOTE: MorphicResult is intentionally declared as a static class (rather than a type) so that nobody tries to create an instance of it accidentally - public static class MorphicResult - { - public static MorphicResultOkValue OkResult(TValue value) - { - return new MorphicResultOkValue(value); - } - - // for Ok results of type MorphicUnit, the overload with no parameters may be called - public static MorphicResultOkValue OkResult() - { - return new MorphicResultOkValue(new MorphicUnit()); - } - - public static MorphicResultErrorValue ErrorResult(TError error) - { - return new MorphicResultErrorValue(error); - } - - // for Error results of type MorphicUnit, the overload with no parameters may be called - public static MorphicResultErrorValue ErrorResult() - { - return new MorphicResultErrorValue(new MorphicUnit()); - } - } - - // the MorphicResultOkValue generic type is provided so that we implicitly cast Ok return values into dual-generic-typed MorphicResult<,> result types - public struct MorphicResultOkValue - { - public T Value { get; private set; } - - public MorphicResultOkValue(T value) - { - this.Value = value; - } - } - - // the MorphicResultErrorValue generic type is provided so that we implicitly cast Error return values into dual-generic-typed MorphicResult<,> result types - public struct MorphicResultErrorValue - { - public T Error { get; private set; } - - public MorphicResultErrorValue(T error) - { - this.Error = error; - } - } -} diff --git a/Morphic.Core/Preferences.cs b/Morphic.Core/Preferences.cs deleted file mode 100644 index a307bb95..00000000 --- a/Morphic.Core/Preferences.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System.Text.Json.Serialization; -using System.Collections.Generic; - -namespace Morphic.Core -{ - public class Preferences: IRecord - { - - /// - /// Default constructor - /// - public Preferences() - { - } - - /// - /// Copy Constructor - /// - /// - public Preferences(Preferences other) - { - if (other.Default is Dictionary otherDefault) - { - Default = new Dictionary(); - foreach (var pair in otherDefault) - { - Default.Add(pair.Key, new SolutionPreferences(pair.Value)); - } - } - } - - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("user_id")] - public string? UserId { get; set; } - - /// The user's default preferences - // Stored as a serialized JSON string in the mongo database because keys might contain dots, - // and mongoDB doesn't allow dots in field keys. Since we're unlikely to need to run queries - // within the solution preferences, we don't lose any functionality by storing serialized JSON. - [JsonPropertyName("default")] - public Dictionary? Default { get; set; } - - public struct Key - { - public string Solution; - public string Preference; - - public Key(string solution, string preference) - { - Solution = solution; - Preference = preference; - } - - public override string ToString() - { - return string.Format("{0}.{1}", Solution, Preference); - } - - public override int GetHashCode() - { - return Solution.GetHashCode() ^ Preference.GetHashCode(); - } - - public override bool Equals(object? obj) - { - if (obj is Key other) - { - return Solution == other.Solution && Preference == other.Preference; - } - return false; - } - } - - public void Set(Key key, object? value) - { - if (Default is null) - { - Default = new Dictionary(); - } - if (!Default.ContainsKey(key.Solution)) - { - Default[key.Solution] = new SolutionPreferences(); - } - Default[key.Solution].Values[key.Preference] = value; - } - - public object? Get(Key key) - { - if (Default is not null) - { - if (Default.TryGetValue(key.Solution, out var preferencesSet)) - { - if (preferencesSet.Values.TryGetValue(key.Preference, out var value)) - { - return value; - } - } - } - return null; - } - - /// - /// Remove the given key from these preferences - /// - /// - public void Remove(Key key) - { - if (Default is not null) - { - if (Default.TryGetValue(key.Solution, out var preferencesSet)) - { - preferencesSet.Values.Remove(key.Preference); - if (preferencesSet.Values.Count == 0) - { - Default.Remove(key.Solution); - } - } - } - } - - /// - /// Get a flat set of preference values keyed by Preferences.Key - /// - /// - public Dictionary GetValuesByKey() - { - var valuesByKey = new Dictionary(); - if (Default is not null) - { - foreach (var solutionPair in Default) - { - foreach (var preferencePair in solutionPair.Value.Values) - { - var key = new Key(solutionPair.Key, preferencePair.Key); - valuesByKey.Add(key, preferencePair.Value); - } - } - } - return valuesByKey; - } - } - - /// Stores preferences for a specific solution - public class SolutionPreferences - { - /// Arbitrary preferences specific to the solution - [JsonExtensionData] - public Dictionary Values { get; set; } = new Dictionary(); - - [JsonIgnore] - public Dictionary? Previous { get; set; } - - /// - /// Default constructor - /// - public SolutionPreferences() - { - } - - /// - /// Copy constructor - /// - /// - public SolutionPreferences(SolutionPreferences other) - { - Values = new Dictionary(other.Values); - } - } -} diff --git a/Morphic.Core/Storage.cs b/Morphic.Core/Storage.cs deleted file mode 100644 index bf103601..00000000 --- a/Morphic.Core/Storage.cs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System; -using System.IO; -using System.Threading.Tasks; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace Morphic.Core -{ - - /// - /// Options for a Storage instance - /// - /// - /// Designed so one or more options can be passed to the Storage constructor via - /// dependency injection - /// - public class StorageOptions - { - public string RootPath = ""; - } - - /// - /// A Storage manager for Morphic objects - /// - public class Storage - { - - /// - /// Create a new storage instance - /// - /// - /// Intended to be created via dependency injection - /// - /// The options for this storage manager - /// A logger for this storage manager - public Storage(StorageOptions options, ILogger logger) - { - this.logger = logger; - RootPath = options.RootPath; - } - - /// - /// The logger used by this storage manager - /// - private readonly ILogger logger; - - /// - /// The root path of the storage area - /// - private readonly string RootPath; - - /// - /// Get a path for the given record identifier and type - /// - /// The record's unique identifier - /// The type of the record - /// - private string PathForRecord(string identifier, Type type) - { - return Path.Combine(new string[] { RootPath, type.Name, String.Format("{0}.json", identifier) }); - } - - /// - /// Save a record to disk - /// - /// The class of record being saved - /// The record being saved - /// Whether or not the save succeeded - public async Task> SaveAsync(RecordType record) where RecordType: class, IRecord - { - var type = typeof(RecordType); - var path = PathForRecord(record.Id, type); - logger.LogInformation("Saving {0}/{1}", type.Name, record.Id); - var parent = Path.GetDirectoryName(path); - try - { - if (!Directory.Exists(parent)) - { - logger.LogInformation("Creating directory {0}", type.Name); - Directory.CreateDirectory(parent); - } - using (var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew, FileAccess.Write)) - { - await JsonSerializer.SerializeAsync(stream, record); - } - logger.LogInformation("Saved {0}/{1}", type.Name, record.Id); - return MorphicResult.OkResult(); - } - catch (Exception e) - { - logger.LogError(e, "Failed to save {0}/{1}", type.Name, record.Id); - return MorphicResult.ErrorResult(); - } - } - - /// - /// Load a record for the given identifier and type - /// - /// The type of record to load - /// The record's unique identifier - /// The requested record, or null if no such record was found - public async Task LoadAsync(string identifier) where RecordType: class, IRecord - { - var type = typeof(RecordType); - var path = PathForRecord(identifier, type); - logger.LogInformation("Loading {0}/{1}", type.Name, identifier); - try - { - if (File.Exists(path)) - { - var options = new JsonSerializerOptions(); - options.Converters.Add(new JsonElementInferredTypeConverter()); - using (var stream = File.OpenRead(path)) - { - var record = await JsonSerializer.DeserializeAsync(stream, options); - return record; - } - } - logger.LogInformation("Not such record {0}/{1}", type.Name, identifier); - return null; - } - catch (Exception e) - { - logger.LogError(e, "Failed to read {0}/{1}", type.Name, identifier); - return null; - } - } - - /// - /// Check if a record exists - /// - /// The type of record to check - /// The record's unique identifier - /// Whether the record is saved on disk or not - public bool Exists(string identifier) where RecordType: class, IRecord - { - var path = PathForRecord(identifier, typeof(RecordType)); - return File.Exists(path); - } - } -} diff --git a/Morphic.Core/TypeConversion.cs b/Morphic.Core/TypeConversion.cs deleted file mode 100644 index a6ce28c2..00000000 --- a/Morphic.Core/TypeConversion.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Morphic.Core -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.Linq; - - public static class TypeConversion - { - /// - /// Convert an object to another type. - /// - /// The value to convert. - /// The value to return, if the conversion fails. - /// The type to convert to. - /// The converted value. - public static T ConvertTo(this object? value, T defaultValue = default) - { - return value.TryConvert(out T result) ? result : defaultValue; - } - - /// - /// Convert an object to another type. - /// - /// The value to convert. - /// The converted value. - /// The type to convert to. - /// true if the conversion was successful. - public static bool TryConvert(this object? value, [NotNullWhen(true)] out T result) - { - bool success; - object? resultObject; - - if (value is null) - { - result = default!; - return false; - } - - if (value is T v) - { - resultObject = v; - success = true; - } - else if (typeof(T) == typeof(bool) && value is string stringValue) - { - // See if it's a false-like word, or a zero number. - bool isFalse = new[] { "", "false", "no", "off" }.Contains(stringValue.ToLowerInvariant()); - if (isFalse) - { - resultObject = false; - } - else if (double.TryParse(stringValue, NumberStyles.Any, null, out double number)) - { - resultObject = number != 0; - } - else - { - // Anything else is true. - resultObject = true; - } - success = true; - } - else if (typeof(T) == typeof(string)) - { - resultObject = value.ToString(); - success = true; - } - else - { - try - { - resultObject = Convert.ChangeType(value, typeof(T)); - success = true; - } - catch (Exception e) when (e is FormatException || e is InvalidCastException) - { - resultObject = default!; - success = false; - } - } - - if (success && resultObject is T o) - { - result = o; - } - else - { - result = default!; - success = false; - } - - return success; - } - } -} diff --git a/Morphic.Core/User.cs b/Morphic.Core/User.cs deleted file mode 100644 index fd31dac1..00000000 --- a/Morphic.Core/User.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using System.Text.Json.Serialization; - -namespace Morphic.Core -{ - public class User: IRecord - { - [JsonPropertyName("id")] - public string Id { get; set; } = ""; - - [JsonPropertyName("preferences_id")] - public string? PreferencesId { get; set; } - - [JsonPropertyName("email")] - public string? Email { get; set; } - - [JsonPropertyName("first_name")] - public string? FirstName { get; set; } - - [JsonPropertyName("last_name")] - public string? LastName { get; set; } - } -} diff --git a/Morphic.Core/UsernameCredentials.cs b/Morphic.Core/UsernameCredentials.cs deleted file mode 100644 index bcd048ce..00000000 --- a/Morphic.Core/UsernameCredentials.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2020 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -namespace Morphic.Core -{ - /// - /// Username/password based credentials - /// - public class UsernameCredentials : ICredentials - { - public UsernameCredentials(string username, string password) - { - Username = username; - Password = password; - } - - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/Morphic.Integrations.Office/Morphic.Integrations.Office.csproj b/Morphic.Integrations.Office/Morphic.Integrations.Office.csproj deleted file mode 100644 index c9d308ed..00000000 --- a/Morphic.Integrations.Office/Morphic.Integrations.Office.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - netcoreapp3.1 - 9.0 - enable - AnyCPU;x64 - - - - - - - - - - - - - - - - - - - - C:\Program Files (x86)\Microsoft Visual Studio\Shared\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.Word.dll - - - - diff --git a/Morphic.Integrations.Office/Templates/Word_ComponentTemplates.xml b/Morphic.Integrations.Office/Templates/Word_ComponentTemplates.xml deleted file mode 100644 index 20e7ebcf..00000000 --- a/Morphic.Integrations.Office/Templates/Word_ComponentTemplates.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Integrations.Office/Templates/Word_EmptyTemplate.xml b/Morphic.Integrations.Office/Templates/Word_EmptyTemplate.xml deleted file mode 100644 index cc3d9647..00000000 --- a/Morphic.Integrations.Office/Templates/Word_EmptyTemplate.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Integrations.Office/WordRibbon.cs b/Morphic.Integrations.Office/WordRibbon.cs deleted file mode 100644 index d848ee5f..00000000 --- a/Morphic.Integrations.Office/WordRibbon.cs +++ /dev/null @@ -1,556 +0,0 @@ -// Copyright 2020-2021 Raising the Floor - International -// -// Licensed under the New BSD license. You may not use this file except in -// compliance with this License. -// -// You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt -// -// The R&D leading to these results received funding from the: -// * Rehabilitation Services Administration, US Dept. of Education under -// grant H421A150006 (APCP) -// * National Institute on Disability, Independent Living, and -// Rehabilitation Research (NIDILRR) -// * Administration for Independent Living & Dept. of Education under grants -// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) -// * European Union's Seventh Framework Programme (FP7/2007-2013) grant -// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) -// * William and Flora Hewlett Foundation -// * Ontario Ministry of Research and Innovation -// * Canadian Foundation for Innovation -// * Adobe Foundation -// * Consumer Electronics Association Foundation - -using Morphic.Core; -using Microsoft.Office.Interop.Word; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using System.Runtime.InteropServices; -using System.Xml; -using System.Diagnostics; -using System.Threading.Tasks; - -namespace Morphic.Integrations.Office -{ - public class WordRibbon - { - private const string BASIC_SIMPLIFY_RIBBON_ID = "morphic.basics"; - private const string ESSENTIALS_SIMPLIFY_RIBBON_ID = "morphic.essentials"; - - private const string MSO_NAMESPACE = "http://schemas.microsoft.com/office/2009/07/customui"; - - - #region General Office functions - - // NOTE: if we add more Word- or Office-related functionality, we should move this region to a separate class - public static bool IsOfficeInstalled() - { - var path = WordRibbon.GetPathToOfficeUserDataDirectory(); - return System.IO.Directory.Exists(path); - } - - private static string GetPathToOfficeUserDataDirectory() - { - return System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Office"); - } - - #endregion General Office functions - - #region General Word functions - - public static MorphicResult IsWordRunning() - { - Process[] processes; - try - { - processes = Process.GetProcessesByName("WINWORD"); - } - catch - { - // we could not detect whether or not Word is running - return MorphicResult.ErrorResult(); - } - - if (processes.Length > 0) - { - return MorphicResult.OkResult(true); - } - else - { - return MorphicResult.OkResult(false); - } - } - - #endregion General Word functions - - private static string GetPathToWordRibbonFile() - { - return System.IO.Path.Combine(WordRibbon.GetPathToOfficeUserDataDirectory(), "Word.officeUI"); - } - - // - - public static MorphicResult IsBasicSimplifyRibbonEnabled() - { - return WordRibbon.IsRibbonEnabled(WordRibbon.BASIC_SIMPLIFY_RIBBON_ID); - } - - public static MorphicResult IsEssentialsSimplifyRibbonEnabled() - { - return WordRibbon.IsRibbonEnabled(WordRibbon.ESSENTIALS_SIMPLIFY_RIBBON_ID); - } - - private static MorphicResult IsRibbonEnabled(string ribbonId) - { - var path = GetPathToWordRibbonFile(); - if (System.IO.File.Exists(path) == false) - { - return MorphicResult.OkResult(false); - } - - var xmlDocument = new XmlDocument(); - try - { - xmlDocument.Load(path); - } - catch - { - return MorphicResult.ErrorResult(); - } - - XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable); - xmlNamespaceManager.AddNamespace("mso", WordRibbon.MSO_NAMESPACE); - - var msoTabsParentNode = xmlDocument.SelectSingleNode("mso:customUI/mso:ribbon/mso:tabs", xmlNamespaceManager); - if (msoTabsParentNode is null) - { - // parent tabs node doesn't exist, so the ribbon is not enabled - return MorphicResult.OkResult(false); - } - - var msoTabNodes = xmlDocument.SelectNodes("mso:customUI/mso:ribbon/mso:tabs/mso:tab", xmlNamespaceManager); - if (msoTabNodes is null) - { - // child tab nodes don't exist, so the ribbon is not enabled - return MorphicResult.OkResult(false); - } - - foreach (XmlNode? msoTabNode in msoTabNodes!) - { - if (msoTabNode?.Attributes["id"].Value == ribbonId) - { - return MorphicResult.OkResult(true); - } - } - - // if we did not find the tab in our list, return false - return MorphicResult.OkResult(false); - } - - // - - public static MorphicResult DisableBasicSimplifyRibbon() - { - var disableRibbonResult = WordRibbon.DisableRibbon(WordRibbon.BASIC_SIMPLIFY_RIBBON_ID); - if (disableRibbonResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - // NOTE: we have removed this functionality for the time being due to limitations in Word automation; we may revisit it in the future - //await WordRibbon.ReloadRibbonsAsync(); - - return MorphicResult.OkResult(); - } - - public static MorphicResult DisableEssentialsSimplifyRibbon() - { - var disableRibbonResult = WordRibbon.DisableRibbon(WordRibbon.ESSENTIALS_SIMPLIFY_RIBBON_ID); - if (disableRibbonResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - // NOTE: we have removed this functionality for the time being due to limitations in Word automation; we may revisit it in the future - //await WordRibbon.ReloadRibbonsAsync(); - - return MorphicResult.OkResult(); - } - - // NOTE: this function does not make Word update its ribbons in real-time; to do so, call the ReloadRibbons() function after using this function - private static MorphicResult DisableRibbon(string ribbonId) - { - var path = GetPathToWordRibbonFile(); - if (System.IO.File.Exists(path) == false) - { - return MorphicResult.OkResult(); - } - - var xmlDocument = new XmlDocument(); - try - { - xmlDocument.Load(path); - } - catch - { - return MorphicResult.ErrorResult(); - } - - XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable); - xmlNamespaceManager.AddNamespace("mso", WordRibbon.MSO_NAMESPACE); - - var msoTabsParentNode = xmlDocument.SelectSingleNode("mso:customUI/mso:ribbon/mso:tabs", xmlNamespaceManager); - if (msoTabsParentNode is null) - { - // parent tabs node doesn't exist, so the ribbon also does not exist - return MorphicResult.OkResult(); - } - - var msoTabNodes = xmlDocument.SelectNodes("mso:customUI/mso:ribbon/mso:tabs/mso:tab", xmlNamespaceManager); - if (msoTabNodes is null) - { - // child tab nodes don't exist, so the ribbon also does not exist - return MorphicResult.OkResult(); - } - - var ribbonRemoved = false; - foreach (XmlNode? msoTabNode in msoTabNodes!) - { - if (msoTabNode?.Attributes["id"].Value == ribbonId) - { - msoTabNode.ParentNode.RemoveChild(msoTabNode); - ribbonRemoved = true; - } - } - - if (ribbonRemoved == true) - { - // save out the modified XMLdocument - try - { - xmlDocument.Save(path); - } - catch - { - return MorphicResult.ErrorResult(); - } - } - - // if we reach here, the ribbon is not enabled; return success - return MorphicResult.OkResult(); - } - - // - - public static MorphicResult EnableBasicSimplifyRibbon() - { - var disableRibbonResult = WordRibbon.EnableRibbon(WordRibbon.BASIC_SIMPLIFY_RIBBON_ID); - if (disableRibbonResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - // NOTE: we have removed this functionality for the time being due to limitations in Word automation; we may revisit it in the future - //await WordRibbon.ReloadRibbons(); - - return MorphicResult.OkResult(); - } - - public static MorphicResult EnableEssentialsSimplifyRibbon() - { - var disableRibbonResult = WordRibbon.EnableRibbon(WordRibbon.ESSENTIALS_SIMPLIFY_RIBBON_ID, WordRibbon.BASIC_SIMPLIFY_RIBBON_ID /* insertAfterRibbonId */); - if (disableRibbonResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - - // NOTE: we have removed this functionality for the time being due to limitations in Word automation; we may revisit it in the future - //await WordRibbon.ReloadRibbons(); - - return MorphicResult.OkResult(); - } - - // NOTE: this function does not make Word update its ribbons in real-time; to do so, call the ReloadRibbons() function after using this function - private static MorphicResult EnableRibbon(string ribbonId, string? insertAfterRibbonId = null) - { - XmlDocument xmlDocument = new XmlDocument(); - - var path = GetPathToWordRibbonFile(); - if (System.IO.File.Exists(path) == true) - { - try - { - xmlDocument.Load(path); - } - catch - { - return MorphicResult.ErrorResult(); - } - } - - XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable); - xmlNamespaceManager.AddNamespace("mso", WordRibbon.MSO_NAMESPACE); - - // verify that the "mso:customUI" node exists - var msoCustomUINode = xmlDocument.SelectSingleNode("mso:customUI", xmlNamespaceManager); - if (msoCustomUINode is null) - { - // required root note doesn't exist - - // does a DIFFERENT root node exist? If so, exit with failure - var rootNode = xmlDocument.FirstChild; - if (rootNode is not null) - { - return MorphicResult.ErrorResult(); - } - - // otherwise, if the XML tree is empty, create the required root node - msoCustomUINode = xmlDocument.CreateNode(XmlNodeType.Element, "mso:customUI", WordRibbon.MSO_NAMESPACE); - xmlDocument.AppendChild(msoCustomUINode); - } - - // verify that the "mso:customUI/mso:ribbon" node exists - var msoRibbonNode = xmlDocument.SelectSingleNode("mso:customUI/mso:ribbon", xmlNamespaceManager); - if (msoRibbonNode is null) - { - // required ribbon node doesn't exist; create it now - msoRibbonNode = xmlDocument.CreateNode(XmlNodeType.Element, "mso:ribbon", WordRibbon.MSO_NAMESPACE); - msoCustomUINode.AppendChild(msoRibbonNode); - } - - // verify that the "mso:customUI/mso:ribbon/mso:tabs" node exists - var msoTabsParentNode = xmlDocument.SelectSingleNode("mso:customUI/mso:ribbon/mso:tabs", xmlNamespaceManager); - if (msoTabsParentNode is null) - { - // required tabs node doesn't exist; create it now - msoTabsParentNode = xmlDocument.CreateNode(XmlNodeType.Element, "mso:tabs", WordRibbon.MSO_NAMESPACE); - msoRibbonNode.AppendChild(msoTabsParentNode); - } - - // verify that the tab (ribbon) is not already present; if it is, then return success - var msoTabNodes = xmlDocument.SelectNodes("mso:customUI/mso:ribbon/mso:tabs/mso:tab", xmlNamespaceManager); - if (msoTabNodes is not null) - { - foreach (XmlNode? msoTabNode in msoTabNodes!) - { - if (msoTabNode?.Attributes["id"].Value == ribbonId) - { - return MorphicResult.OkResult(); - } - } - } - - // at this point, we know that the tab (i.e. ribbon) is not in the XmlDocument; load it from our template and insert it now - - // get a copy of the appropriate tab (ribbon) node from the template we have embedded as a resource in this library - var getRibbonNodeResult = WordRibbon.GetRibbonTabNodeFromTemplate(ribbonId); - if (getRibbonNodeResult.IsError == true) - { - // programming error - throw new Exception("Error: could not get ribbon (resource '" + ribbonId + "')"); - } - var ribbonNodeToImport = getRibbonNodeResult.Value!; - - XmlNode? insertAfterNode = null; - if (insertAfterRibbonId is not null) - { - var getRibbonResult = GetRibbonTabNodeFromXmlDocument(xmlDocument, insertAfterRibbonId!); - if ((getRibbonResult.IsSuccess == true) && (getRibbonResult.Value is not null)) - { - insertAfterNode = getRibbonResult.Value!; - } - } - - // insert the tab (ribbon) node at the top of our list (or in the appropriate place, if it should go _after_ another node - // TODO: we actually want to insert ESSENTIALS _after_ BASIC if BASIC exists; add some logic for that! - var ribbonNode = xmlDocument.ImportNode(ribbonNodeToImport, true); - if (insertAfterNode is not null) - { - msoTabsParentNode.InsertAfter(ribbonNode, insertAfterNode!); - } - else - { - msoTabsParentNode.InsertBefore(ribbonNode, msoTabsParentNode.FirstChild); - } - - // save out the modified XMLdocument - try - { - xmlDocument.Save(path); - } - catch - { - return MorphicResult.ErrorResult(); - } - - // if we reach here, the ribbon has been enabled; return success - return MorphicResult.OkResult(); - } - - // - - private static MorphicResult GetRibbonTabNodeFromXmlDocument(XmlDocument xmlDocument, string ribbonId) - { - XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable); - xmlNamespaceManager.AddNamespace("mso", WordRibbon.MSO_NAMESPACE); - - var msoTabsParentNode = xmlDocument.SelectSingleNode("mso:customUI/mso:ribbon/mso:tabs", xmlNamespaceManager); - if (msoTabsParentNode is null) - { - // parent tabs node doesn't exist; we are missing our template ribbons - return MorphicResult.ErrorResult(); - } - - var msoTabNodes = xmlDocument.SelectNodes("mso:customUI/mso:ribbon/mso:tabs/mso:tab", xmlNamespaceManager); - if (msoTabNodes is null) - { - // child tab nodes (i.e. ribbon templates) don't exist - return MorphicResult.ErrorResult(); - } - - foreach (XmlNode? msoTabNode in msoTabNodes!) - { - if (msoTabNode?.Attributes["id"].Value == ribbonId) - { - return MorphicResult.OkResult(msoTabNode); - } - } - - // if we did not find the ribbon in our list of tabs, return null - return MorphicResult.OkResult(null); - } - - private static MorphicResult GetRibbonTabNodeFromTemplate(string ribbonId) - { - var ribbonTemplateFileStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Morphic.Integrations.Office.Templates.Word_ComponentTemplates.xml"); - if (ribbonTemplateFileStream is null) - { - return MorphicResult.ErrorResult(); - } - - var xmlDocument = new XmlDocument(); - try - { - xmlDocument.Load(ribbonTemplateFileStream); - } - catch - { - return MorphicResult.ErrorResult(); - } - - var ribbonTabNodeResult = WordRibbon.GetRibbonTabNodeFromXmlDocument(xmlDocument, ribbonId); - if (ribbonTabNodeResult.IsError == true) - { - return MorphicResult.ErrorResult(); - } - var ribbonTabNode = ribbonTabNodeResult.Value; - if (ribbonTabNode is null) - { - // if we did not find the tab in our list, return an error - return MorphicResult.ErrorResult(); - } - else - { - return MorphicResult.OkResult(ribbonTabNode!); - } - } - - // - - // NOTE: we have removed this functionality for the time being due to limitations in Word automation; we may revisit it in the future - //private static async Task> ReloadRibbonsAsync() - //{ - // Microsoft.Office.Interop.Word.Application? wordApplication; - // try - // { - // wordApplication = Morphic.Windows.Native.InteropServices.Marshal.GetActiveObject("Word.Application") as Microsoft.Office.Interop.Word.Application; - // } - // catch (COMException) - // { - // // if we get a COM exception, assume that Word is not installed - // return MorphicResult.OkResult(); - // } - - // if (wordApplication is null) - // { - // // if Word was not running, the object will be null; there is nothing to do - // return MorphicResult.OkResult(); - // } - - // if (wordApplication.Windows.Count == 0) - // { - // // if Word has no active windows, there is nothing to do - // return MorphicResult.OkResult(); - // } - - // var isSuccess = true; - - // try - // { - // // create a new Word window; we'll then toggle activation between all active Word windows (and then close the new window) to convince Word to refresh its ribbons in all windows - // // NOTE: this is a bit of a hack, based on observations during the development of the original Morphic software; ideally we can find a proper programmatic way to convince Word to refresh in the future - - // // first, capture a reference to the active Word window; we'll need to return to this window after our cycling is done - // var activeWordWindow = wordApplication.ActiveWindow; - - // try - // { - // // create a new window; by observation, this will cause Word to load in the new ribbons - // // NOTE: if there were already _multiple_ windows, it _might_ be sufficient to just cycle through existing windows to trigger the reload - // var tempWordWindow = wordApplication.NewWindow(); - - // // OBSERVATION: this pattern of switching between windows is based on word derived from earlier Morphic Classic code; we should re-evaluate this methodology, to determine if it is still the best - // // way to trigger Word to let it know that the ribbons file has been updated - - // // cycle through each Word window...twice - // for (var cycleIndex = 0; cycleIndex < 2; cycleIndex += 1) - // { - // foreach (Window? wordWindow in wordApplication.Windows) - // { - // if (wordWindow is null) - // { - // Debug.Assert(false, "Programming error: Word's Windows array is not null, but it is returning null windows"); - // continue; - // } - - // //Morphic.Windows.Native.SendMessage - // var nativeWordWindow = new Morphic.Windows.Native.Windowing.Window((IntPtr)wordWindow.Hwnd); - // var sendMessageResult = nativeWordWindow.Activate(); - // // NOTE: this 250m delay is somewhat arbitrary; we should do further examination to find the right delay (or to ask Word the right questions to know when it is done) - // await System.Threading.Tasks.Task.Delay(250); - // } - // } - - // // finally, activate both the original window and the new window _twice_, and then close the new window - // for (var cycleIndex = 0; cycleIndex < 2; cycleIndex += 1) - // { - // activeWordWindow.Activate(); - // // NOTE: this 250m delay is somewhat arbitrary; we should do further examination to find the right delay (or to ask Word the right questions to know when it is done) - // await System.Threading.Tasks.Task.Delay(250); - // tempWordWindow.Activate(); - // // NOTE: this 250m delay is somewhat arbitrary; we should do further examination to find the right delay (or to ask Word the right questions to know when it is done) - // await System.Threading.Tasks.Task.Delay(250); - // } - // // - // tempWordWindow.Close(); - // } - // finally - // { - // // for sanity sake, make sure we end up with the original Word window being active - // activeWordWindow.Activate(); - // } - // } - // catch - // { - // // if any operations fail (via COM automation), return an error - // isSuccess = false; - // } - - // // if we completed all the steps, assume success - // return isSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); - //} - } -} diff --git a/Morphic.ManualTester/App.xaml b/Morphic.ManualTester/App.xaml deleted file mode 100644 index f19fb3e0..00000000 --- a/Morphic.ManualTester/App.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Morphic.ManualTester/App.xaml.cs b/Morphic.ManualTester/App.xaml.cs deleted file mode 100644 index 50f3a685..00000000 --- a/Morphic.ManualTester/App.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Morphic.ManualTester -{ - using System.Windows; - - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - } -} diff --git a/Morphic.ManualTester/AssemblyInfo.cs b/Morphic.ManualTester/AssemblyInfo.cs deleted file mode 100644 index 8b5504ec..00000000 --- a/Morphic.ManualTester/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/Morphic.ManualTester/Hourglass.png b/Morphic.ManualTester/Hourglass.png deleted file mode 100644 index 5c66f770..00000000 Binary files a/Morphic.ManualTester/Hourglass.png and /dev/null differ diff --git a/Morphic.ManualTester/Icon.ico b/Morphic.ManualTester/Icon.ico deleted file mode 100644 index 633cab33..00000000 Binary files a/Morphic.ManualTester/Icon.ico and /dev/null differ diff --git a/Morphic.ManualTester/Icon.png b/Morphic.ManualTester/Icon.png deleted file mode 100644 index 497994e4..00000000 Binary files a/Morphic.ManualTester/Icon.png and /dev/null differ diff --git a/Morphic.ManualTester/MainWindow.xaml b/Morphic.ManualTester/MainWindow.xaml deleted file mode 100644 index 3afd8f76..00000000 --- a/Morphic.ManualTester/MainWindow.xaml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Manual Settings Tester - - - -