From dfbb77c02ec3bf66fa1a97beb7a6ff19a1607159 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 24 Feb 2021 19:47:09 -0400 Subject: [PATCH 01/74] Initial commit for net60 arm64/x86/x64 build --- .gitignore | 9 - LICENSE.txt | 36 +- Morphic.Client/App.xaml | 10 - Morphic.Client/App.xaml.cs | 935 ----------- Morphic.Client/AssemblyInfo.cs | 10 - .../Assets/bar-icons/amazon-brands.svg | 2 - .../Assets/bar-icons/calendar-solid.svg | 1 - .../Assets/bar-icons/camera-solid.svg | 2 - .../Assets/bar-icons/comment-solid.svg | 2 - .../Assets/bar-icons/envelope-open-text.svg | 1 - .../Assets/bar-icons/envelope-open.svg | 1 - .../bar-icons/envelope-outline-open.svg | 1 - .../Assets/bar-icons/envelope-outline.svg | 1 - .../Assets/bar-icons/envelope-solid.svg | 2 - Morphic.Client/Assets/bar-icons/envelope.svg | 1 - Morphic.Client/Assets/bar-icons/facebook.svg | 39 - Morphic.Client/Assets/bar-icons/gmail.svg | 20 - .../Assets/bar-icons/google-brands.svg | 2 - .../Assets/bar-icons/google-drive-brands.svg | 2 - .../Assets/bar-icons/images-solid.svg | 2 - Morphic.Client/Assets/bar-icons/instagram.svg | 57 - .../Assets/bar-icons/link-solid.svg | 2 - Morphic.Client/Assets/bar-icons/minus.svg | 69 - .../Assets/bar-icons/morphic-logo.svg | 45 - .../Assets/bar-icons/music-solid.svg | 2 - .../Assets/bar-icons/newspaper-solid.svg | 1 - Morphic.Client/Assets/bar-icons/outlook.svg | 36 - Morphic.Client/Assets/bar-icons/paypal.svg | 44 - Morphic.Client/Assets/bar-icons/plus.svg | 59 - .../Assets/bar-icons/question-solid.svg | 1 - Morphic.Client/Assets/bar-icons/reddit.svg | 1 - .../Assets/bar-icons/shopping-cart-solid.svg | 2 - .../Assets/bar-icons/skype-brands.svg | 2 - Morphic.Client/Assets/bar-icons/skype.svg | 46 - Morphic.Client/Assets/bar-icons/telegram.svg | 1 - Morphic.Client/Assets/bar-icons/test/grid.svg | 89 -- Morphic.Client/Assets/bar-icons/twitter.svg | 43 - Morphic.Client/Assets/bar-icons/viber.svg | 1 - .../Assets/bar-icons/video-solid.svg | 2 - Morphic.Client/Assets/bar-icons/whatsapp.svg | 45 - .../Assets/bar-icons/yahoo-mail.svg | 1 - Morphic.Client/Assets/bar-icons/youtube.svg | 1 - Morphic.Client/Assets/morphic-icon.ico | Bin 160213 -> 0 bytes Morphic.Client/Backups.cs | 94 -- Morphic.Client/Bar/AppFocus.cs | 121 -- Morphic.Client/Bar/BarData.md | 519 ------- Morphic.Client/Bar/BarImages.cs | 169 -- Morphic.Client/Bar/BarManager.cs | 362 ----- .../Bar/Data/Actions/ApplicationAction.cs | 356 ----- Morphic.Client/Bar/Data/Actions/BarAction.cs | 342 ---- Morphic.Client/Bar/Data/Actions/Functions.cs | 251 --- .../Bar/Data/Actions/InternalFunctions.cs | 183 --- .../Bar/Data/Actions/SettingAction.cs | 64 - Morphic.Client/Bar/Data/Actions/WebAction.cs | 89 -- Morphic.Client/Bar/Data/BarButton.cs | 242 --- Morphic.Client/Bar/Data/BarData.cs | 355 ----- Morphic.Client/Bar/Data/BarEnums.cs | 50 - Morphic.Client/Bar/Data/BarItem.cs | 226 --- Morphic.Client/Bar/Data/BarItemTheme.cs | 183 --- Morphic.Client/Bar/Data/BarJson.cs | 378 ----- Morphic.Client/Bar/Data/BarMultiButton.cs | 168 -- Morphic.Client/Bar/Data/BarPosition.cs | 370 ----- Morphic.Client/Bar/Data/BarPresets.cs | 112 -- Morphic.Client/Bar/Data/BarSettingItem.cs | 81 - Morphic.Client/Bar/Data/BarSizes.cs | 115 -- Morphic.Client/Bar/Data/SecondaryBar.cs | 34 - Morphic.Client/Bar/UI/AppBarWindow/AppBar.cs | 502 ------ .../Bar/UI/AppBarWindow/AppBarAPI.cs | 236 --- .../Bar/UI/AppBarWindow/WindowMovement.cs | 301 ---- Morphic.Client/Bar/UI/BarControl.cs | 278 ---- .../Bar/UI/BarControls/BarContextMenu.cs | 68 - .../Bar/UI/BarControls/BarItemControl.cs | 208 --- .../Bar/UI/BarControls/ButtonBarControl.xaml | 205 --- .../UI/BarControls/ButtonBarControl.xaml.cs | 67 - .../Bar/UI/BarControls/ImageBarControl.xaml | 47 - .../UI/BarControls/ImageBarControl.xaml.cs | 34 - .../UI/BarControls/MultiButtonBarControl.xaml | 125 -- .../BarControls/MultiButtonBarControl.xaml.cs | 462 ------ .../Bar/UI/BarControls/ThemeHandler.cs | 153 -- Morphic.Client/Bar/UI/BarWindow.xaml | 138 -- Morphic.Client/Bar/UI/BarWindow.xaml.cs | 579 ------- Morphic.Client/Bar/UI/Converters.cs | 106 -- Morphic.Client/Bar/UI/CorrectedCoords.cs | 70 - Morphic.Client/Bar/UI/ExpanderWindow.xaml | 20 - Morphic.Client/Bar/UI/ExpanderWindow.xaml.cs | 232 --- Morphic.Client/Bar/UI/PrimaryBarWindow.cs | 478 ------ Morphic.Client/Bar/UI/QuickHelpWindow.xaml | 65 - Morphic.Client/Bar/UI/QuickHelpWindow.xaml.cs | 400 ----- Morphic.Client/Bar/UI/SecondaryBarWindow.cs | 177 --- Morphic.Client/Bar/UI/TextBlockLimited.cs | 63 - Morphic.Client/Bar/UI/WindowMessageHook.cs | 188 --- Morphic.Client/Config/AppOptions.cs | 473 ------ Morphic.Client/Config/AppPaths.cs | 197 --- Morphic.Client/Config/BuildInfo.cs | 36 - Morphic.Client/Config/ConfigurableFeatures.cs | 52 - Morphic.Client/Config/DataProtector.cs | 62 - Morphic.Client/Config/UpdateOptions.cs | 7 - Morphic.Client/DefaultConfig/basic-bar.json5 | 142 -- .../DefaultConfig/default-bar.json5 | 84 - Morphic.Client/DefaultConfig/presets.json5 | 237 --- .../DefaultConfig/testbar-actions.json5 | 61 - .../DefaultConfig/testbar-all-kinds.json5 | 59 - Morphic.Client/DefaultPreferences.json | 7 - Morphic.Client/Dialogs/AboutWindow.xaml | 26 - Morphic.Client/Dialogs/AboutWindow.xaml.cs | 45 - .../Dialogs/CommunityPickerWindow.xaml | 31 - .../Dialogs/CommunityPickerWindow.xaml.cs | 43 - Morphic.Client/Dialogs/DialogManager.cs | 108 -- .../Dialogs/Elements/PagerControl.xaml | 13 - .../Dialogs/Elements/PagerControl.xaml.cs | 97 -- Morphic.Client/Dialogs/Elements/StepFrame.cs | 174 --- Morphic.Client/Dialogs/LoginAnnounce.wav | Bin 218072 -> 0 bytes Morphic.Client/Dialogs/LoginPanel.xaml | 43 - Morphic.Client/Dialogs/LoginPanel.xaml.cs | 209 --- Morphic.Client/Dialogs/LoginWindow.xaml | 45 - Morphic.Client/Dialogs/LoginWindow.xaml.cs | 60 - Morphic.Client/Dialogs/Travel/ApplyPanel.xaml | 94 -- .../Dialogs/Travel/ApplyPanel.xaml.cs | 102 -- .../Dialogs/Travel/CapturePanel.xaml | 90 -- .../Dialogs/Travel/CapturePanel.xaml.cs | 142 -- .../Dialogs/Travel/CopyStartPanel.xaml | 16 - .../Dialogs/Travel/CopyStartPanel.xaml.cs | 67 - .../Dialogs/Travel/CreateAccountPanel.xaml | 67 - .../Dialogs/Travel/CreateAccountPanel.xaml.cs | 324 ---- .../Dialogs/Travel/RestoreWindow.xaml | 66 - .../Dialogs/Travel/RestoreWindow.xaml.cs | 125 -- .../Dialogs/Travel/TravelCompletedPanel.xaml | 67 - .../Travel/TravelCompletedPanel.xaml.cs | 94 -- .../Dialogs/Travel/TravelWindow.xaml | 45 - .../Dialogs/Travel/TravelWindow.xaml.cs | 78 - Morphic.Client/Icon.ico | Bin 43392 -> 0 bytes Morphic.Client/Icon.png | Bin 6613 -> 0 bytes Morphic.Client/Menu/MorphicHybridTrayIcon.cs | 259 ---- Morphic.Client/Menu/MorphicMenu.xaml | 65 - Morphic.Client/Menu/MorphicMenu.xaml.cs | 278 ---- Morphic.Client/Menu/MorphicMenuItem.cs | 202 --- Morphic.Client/Menu/MorphicTrayButton.cs | 1380 ----------------- Morphic.Client/MessageWatcherNativeWindow.cs | 189 --- Morphic.Client/Morphic.Client.csproj | 162 -- .../Properties/Resources.Designer.cs | 775 --------- Morphic.Client/Properties/Resources.resx | 360 ----- Morphic.Client/Properties/launchSettings.json | 11 - .../Solutions/jaws2019.solutions.json | 666 -------- .../Solutions/jaws2020.solutions.json | 20 - .../Solutions/windows.solutions.json | 503 ------ Morphic.Client/StringDictionarySetting.cs | 43 - Morphic.Client/UserSettings.Designer.cs | 49 - Morphic.Client/UserSettings.settings | 12 - Morphic.Client/WinApi.cs | 1000 ------------ Morphic.Client/WindowsUserSettings.cs | 59 - Morphic.Client/app.manifest | 83 - Morphic.Client/appsettings.Debug.json | 13 - Morphic.Client/appsettings.Development.json | 27 - Morphic.Client/appsettings.Production.json | 28 - Morphic.Client/appsettings.Staging.json | 27 - Morphic.Client/build-info.json | 4 - Morphic.Client/quickstrip.json | 24 - Morphic.Client/solutions.json5 | 106 -- Morphic.Client/test-bar.json5 | 363 ----- Morphic.Core.Tests/JsonExtensionTests.cs | 159 -- Morphic.Core.Tests/KeychainTests.cs | 79 - Morphic.Core.Tests/Morphic.Core.Tests.csproj | 22 - Morphic.Core.Tests/PreferencesTests.cs | 290 ---- Morphic.Core.Tests/StorageTests.cs | 150 -- Morphic.Core.Tests/UserTests.cs | 110 -- Morphic.Core/Community/BarItem.cs | 185 --- Morphic.Core/Community/Community.cs | 45 - Morphic.Core/Community/UserBar.cs | 39 - Morphic.Core/ICredentials.cs | 32 - Morphic.Core/IDataProtection.cs | 47 - Morphic.Core/IMorphicResult.cs | 3 +- Morphic.Core/IRecord.cs | 13 - Morphic.Core/IUserSettings.cs | 27 - Morphic.Core/JsonExtensions.cs | 105 -- Morphic.Core/KeyCredentials.cs | 51 - Morphic.Core/Keychain.cs | 194 --- Morphic.Core/Morphic.Core.csproj | 12 +- Morphic.Core/MorphicUnit.cs | 2 +- Morphic.Core/Preferences.cs | 194 --- Morphic.Core/Storage.cs | 164 -- Morphic.Core/TypeConversion.cs | 96 -- Morphic.Core/User.cs | 45 - Morphic.Core/UsernameCredentials.cs | 40 - Morphic.ManualTester/App.xaml | 8 - Morphic.ManualTester/App.xaml.cs | 11 - Morphic.ManualTester/AssemblyInfo.cs | 10 - Morphic.ManualTester/Hourglass.png | Bin 401 -> 0 bytes Morphic.ManualTester/Icon.ico | Bin 43392 -> 0 bytes Morphic.ManualTester/Icon.png | Bin 6613 -> 0 bytes Morphic.ManualTester/MainWindow.xaml | 44 - Morphic.ManualTester/MainWindow.xaml.cs | 315 ---- .../ManualControlBoolean.xaml | 24 - .../ManualControlBoolean.xaml.cs | 63 - Morphic.ManualTester/ManualControlDouble.xaml | 24 - .../ManualControlDouble.xaml.cs | 100 -- .../ManualControlInteger.xaml | 24 - .../ManualControlInteger.xaml.cs | 100 -- Morphic.ManualTester/ManualControlString.xaml | 24 - .../ManualControlString.xaml.cs | 77 - .../Morphic.ManualTester.csproj | 46 - .../PublishProfiles/Standalone.pubxml | 18 - .../PublishProfiles/Standalone.pubxml.user | 6 - Morphic.ManualTester/SolutionHeader.xaml | 21 - Morphic.ManualTester/SolutionHeader.xaml.cs | 164 -- .../Morphic.ManualTesterCLI.csproj | 15 - Morphic.ManualTesterCLI/Program.cs | 234 --- .../PublishProfiles/Standalone.pubxml | 18 - .../PublishProfiles/Standalone.pubxml.user | 6 - .../Properties/launchSettings.json | 8 - Morphic.ManualTesterCLI/RegistryManager.cs | 159 -- Morphic.Service/AuthService.cs | 242 --- .../HttpRequestMessageExtensions.cs | 55 - .../HttpResponseMessageExtensions.cs | 87 -- Morphic.Service/HttpService.cs | 198 --- .../IHttpServiceCredentialsProvider.cs | 35 - Morphic.Service/Morphic.Service.csproj | 19 - Morphic.Service/MorphicSession.cs | 377 ----- Morphic.Service/PreferencesService.cs | 63 - Morphic.Service/Session.cs | 160 -- Morphic.Service/UserService.cs | 80 - .../Morphic.Settings.Tests.csproj | 22 - .../Resolvers/ResolverTests.cs | 206 --- .../Display/DisplaySettingsHandlerTests.cs | 18 - .../Ini/IniFileSettingsHandlerTests.cs | 206 --- .../SettingsHandlers/Ini/IniFileTests.cs | 225 --- .../SettingsHandlers/Ini/handler-test.ini | 19 - .../SettingsHandlers/Ini/read-test.ini | 608 -------- .../Ini/write-test.expect.ini | 233 --- .../SettingsHandlers/Ini/write-test.ini | 219 --- .../Registry/RegistrySettingsHandlerTests.cs | 112 -- .../SettingsHandlers/test-solutions.json5 | 46 - .../SolutionsRegistry/SolutionsTests.cs | 29 - Morphic.Settings.Tests/TestUtil.cs | 108 -- Morphic.Settings/Morphic.Settings.csproj | 23 - .../Resolvers/EnvironmentResolver.cs | 17 - Morphic.Settings/Resolvers/FolderResolver.cs | 32 - .../Resolvers/RegistryResolver.cs | 55 - Morphic.Settings/Resolvers/Resolver.cs | 101 -- Morphic.Settings/Resolvers/ResolvingString.cs | 34 - .../Display/DisplayHandler.cs | 129 -- .../SettingsHandlers/FixedSettingsHandler.cs | 162 -- .../SettingsHandlers/Ini/IniFile.cs | 297 ---- .../SettingsHandlers/Ini/IniFileReader.cs | 52 - .../SettingsHandlers/Ini/IniFileRegex.cs | 127 -- .../SettingsHandlers/Ini/IniFileWriter.cs | 304 ---- .../Ini/IniSettingsHandler.cs | 64 - .../SettingsHandlers/Ini/README.md | 99 -- .../Process/ProcessSettingGroup.cs | 15 - .../Process/ProcessSettingsHandler.cs | 74 - .../Registry/RegistrySettingGroup.cs | 29 - .../Registry/RegistrySettingsHandler.cs | 149 -- Morphic.Settings/SettingsHandlers/Setting.cs | 362 ----- .../SettingsHandlers/SettingGroup.cs | 71 - .../SettingsHandlers/SettingsHandler.cs | 113 -- .../SystemSettings/ISystemSettingItem.cs | 140 -- .../SystemSettings/SystemSettingItem.cs | 299 ---- .../SystemSettings/SystemSettingsHandler.cs | 56 - .../Theme/ThemeSettingGroup.cs | 17 - .../Theme/ThemeSettingsHandler.cs | 167 -- Morphic.Settings/SettingsHandlers/Values.cs | 41 - .../SolutionsRegistry/JsonHelpers.cs | 101 -- .../SolutionsRegistry/SettingId.cs | 28 - .../SolutionsRegistry/Solution.cs | 115 -- .../SolutionsRegistry/SolutionServices.cs | 75 - .../SolutionsRegistry/Solutions.cs | 182 --- .../SolutionsRegistry/TypeResolver.cs | 101 -- Morphic.Setup/License.rtf | 45 - Morphic.Setup/Morphic.Setup.wixproj | 74 - Morphic.Setup/Product.wxs | 68 - Morphic.Windows.Native/Audio/AudioEndpoint.cs | 120 -- Morphic.Windows.Native/Display/Display.cs | 754 --------- Morphic.Windows.Native/Input/Keyboard.cs | 63 - Morphic.Windows.Native/Input/Mouse.cs | 34 - .../Morphic.Windows.Native.csproj | 11 - Morphic.Windows.Native/Processor/Processor.cs | 67 - .../Speech/SelectionReader.cs | 184 --- Morphic.Windows.Native/Spi.cs | 353 ----- Morphic.Windows.Native/WindowsApi.cs | 1080 ------------- Morphic.Windows.Native/WindowsCom/Appx.cs | 101 -- Morphic.Windows.Native/WindowsCom/CLSCTX.cs | 65 - .../WindowsCoreAudio/EDataFlow.cs | 36 - .../WindowsCoreAudio/ERole.cs | 36 - .../NoDeviceIsAvailableException.cs | 31 - .../WindowsCoreAudio/IAudioEndpointVolume.cs | 88 -- .../WindowsCoreAudio/IMMDevice.cs | 46 - .../WindowsCoreAudio/IMMDeviceEnumerator.cs | 48 - .../WindowsCoreAudio/MMDevice.cs | 66 - .../WindowsCoreAudio/MMDeviceEnumerator.cs | 95 -- .../WindowsSession/WindowsSession.cs | 35 - MorphicWin.sln | 120 -- README.md | 32 - azure-pipelines.yml | 264 ---- documentation/configuration.md | 42 - documentation/solutions.md | 221 --- set-build-info.sh | 25 - 295 files changed, 35 insertions(+), 36881 deletions(-) delete mode 100644 .gitignore delete mode 100644 Morphic.Client/App.xaml delete mode 100644 Morphic.Client/App.xaml.cs delete mode 100644 Morphic.Client/AssemblyInfo.cs delete mode 100644 Morphic.Client/Assets/bar-icons/amazon-brands.svg delete mode 100644 Morphic.Client/Assets/bar-icons/calendar-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/camera-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/comment-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope-open-text.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope-open.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope-outline-open.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope-outline.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/envelope.svg delete mode 100644 Morphic.Client/Assets/bar-icons/facebook.svg delete mode 100644 Morphic.Client/Assets/bar-icons/gmail.svg delete mode 100644 Morphic.Client/Assets/bar-icons/google-brands.svg delete mode 100644 Morphic.Client/Assets/bar-icons/google-drive-brands.svg delete mode 100644 Morphic.Client/Assets/bar-icons/images-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/instagram.svg delete mode 100644 Morphic.Client/Assets/bar-icons/link-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/minus.svg delete mode 100644 Morphic.Client/Assets/bar-icons/morphic-logo.svg delete mode 100644 Morphic.Client/Assets/bar-icons/music-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/newspaper-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/outlook.svg delete mode 100644 Morphic.Client/Assets/bar-icons/paypal.svg delete mode 100644 Morphic.Client/Assets/bar-icons/plus.svg delete mode 100644 Morphic.Client/Assets/bar-icons/question-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/reddit.svg delete mode 100644 Morphic.Client/Assets/bar-icons/shopping-cart-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/skype-brands.svg delete mode 100644 Morphic.Client/Assets/bar-icons/skype.svg delete mode 100644 Morphic.Client/Assets/bar-icons/telegram.svg delete mode 100644 Morphic.Client/Assets/bar-icons/test/grid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/twitter.svg delete mode 100644 Morphic.Client/Assets/bar-icons/viber.svg delete mode 100644 Morphic.Client/Assets/bar-icons/video-solid.svg delete mode 100644 Morphic.Client/Assets/bar-icons/whatsapp.svg delete mode 100644 Morphic.Client/Assets/bar-icons/yahoo-mail.svg delete mode 100644 Morphic.Client/Assets/bar-icons/youtube.svg delete mode 100644 Morphic.Client/Assets/morphic-icon.ico delete mode 100644 Morphic.Client/Backups.cs delete mode 100644 Morphic.Client/Bar/AppFocus.cs delete mode 100644 Morphic.Client/Bar/BarData.md delete mode 100644 Morphic.Client/Bar/BarImages.cs delete mode 100644 Morphic.Client/Bar/BarManager.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/ApplicationAction.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/BarAction.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/Functions.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/InternalFunctions.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/SettingAction.cs delete mode 100644 Morphic.Client/Bar/Data/Actions/WebAction.cs delete mode 100644 Morphic.Client/Bar/Data/BarButton.cs delete mode 100644 Morphic.Client/Bar/Data/BarData.cs delete mode 100644 Morphic.Client/Bar/Data/BarEnums.cs delete mode 100644 Morphic.Client/Bar/Data/BarItem.cs delete mode 100644 Morphic.Client/Bar/Data/BarItemTheme.cs delete mode 100644 Morphic.Client/Bar/Data/BarJson.cs delete mode 100644 Morphic.Client/Bar/Data/BarMultiButton.cs delete mode 100644 Morphic.Client/Bar/Data/BarPosition.cs delete mode 100644 Morphic.Client/Bar/Data/BarPresets.cs delete mode 100644 Morphic.Client/Bar/Data/BarSettingItem.cs delete mode 100644 Morphic.Client/Bar/Data/BarSizes.cs delete mode 100644 Morphic.Client/Bar/Data/SecondaryBar.cs delete mode 100644 Morphic.Client/Bar/UI/AppBarWindow/AppBar.cs delete mode 100644 Morphic.Client/Bar/UI/AppBarWindow/AppBarAPI.cs delete mode 100644 Morphic.Client/Bar/UI/AppBarWindow/WindowMovement.cs delete mode 100644 Morphic.Client/Bar/UI/BarControl.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/BarContextMenu.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/BarItemControl.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml delete mode 100644 Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml delete mode 100644 Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml delete mode 100644 Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/BarControls/ThemeHandler.cs delete mode 100644 Morphic.Client/Bar/UI/BarWindow.xaml delete mode 100644 Morphic.Client/Bar/UI/BarWindow.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/Converters.cs delete mode 100644 Morphic.Client/Bar/UI/CorrectedCoords.cs delete mode 100644 Morphic.Client/Bar/UI/ExpanderWindow.xaml delete mode 100644 Morphic.Client/Bar/UI/ExpanderWindow.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/PrimaryBarWindow.cs delete mode 100644 Morphic.Client/Bar/UI/QuickHelpWindow.xaml delete mode 100644 Morphic.Client/Bar/UI/QuickHelpWindow.xaml.cs delete mode 100644 Morphic.Client/Bar/UI/SecondaryBarWindow.cs delete mode 100644 Morphic.Client/Bar/UI/TextBlockLimited.cs delete mode 100644 Morphic.Client/Bar/UI/WindowMessageHook.cs delete mode 100644 Morphic.Client/Config/AppOptions.cs delete mode 100644 Morphic.Client/Config/AppPaths.cs delete mode 100644 Morphic.Client/Config/BuildInfo.cs delete mode 100644 Morphic.Client/Config/ConfigurableFeatures.cs delete mode 100644 Morphic.Client/Config/DataProtector.cs delete mode 100644 Morphic.Client/Config/UpdateOptions.cs delete mode 100644 Morphic.Client/DefaultConfig/basic-bar.json5 delete mode 100644 Morphic.Client/DefaultConfig/default-bar.json5 delete mode 100644 Morphic.Client/DefaultConfig/presets.json5 delete mode 100644 Morphic.Client/DefaultConfig/testbar-actions.json5 delete mode 100644 Morphic.Client/DefaultConfig/testbar-all-kinds.json5 delete mode 100644 Morphic.Client/DefaultPreferences.json delete mode 100644 Morphic.Client/Dialogs/AboutWindow.xaml delete mode 100644 Morphic.Client/Dialogs/AboutWindow.xaml.cs delete mode 100644 Morphic.Client/Dialogs/CommunityPickerWindow.xaml delete mode 100644 Morphic.Client/Dialogs/CommunityPickerWindow.xaml.cs delete mode 100644 Morphic.Client/Dialogs/DialogManager.cs delete mode 100644 Morphic.Client/Dialogs/Elements/PagerControl.xaml delete mode 100644 Morphic.Client/Dialogs/Elements/PagerControl.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Elements/StepFrame.cs delete mode 100644 Morphic.Client/Dialogs/LoginAnnounce.wav delete mode 100644 Morphic.Client/Dialogs/LoginPanel.xaml delete mode 100644 Morphic.Client/Dialogs/LoginPanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/LoginWindow.xaml delete mode 100644 Morphic.Client/Dialogs/LoginWindow.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/ApplyPanel.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/ApplyPanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/CapturePanel.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/CapturePanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/CopyStartPanel.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/CopyStartPanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/RestoreWindow.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/RestoreWindow.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/TravelCompletedPanel.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/TravelCompletedPanel.xaml.cs delete mode 100644 Morphic.Client/Dialogs/Travel/TravelWindow.xaml delete mode 100644 Morphic.Client/Dialogs/Travel/TravelWindow.xaml.cs delete mode 100644 Morphic.Client/Icon.ico delete mode 100644 Morphic.Client/Icon.png delete mode 100644 Morphic.Client/Menu/MorphicHybridTrayIcon.cs delete mode 100644 Morphic.Client/Menu/MorphicMenu.xaml delete mode 100644 Morphic.Client/Menu/MorphicMenu.xaml.cs delete mode 100644 Morphic.Client/Menu/MorphicMenuItem.cs delete mode 100644 Morphic.Client/Menu/MorphicTrayButton.cs delete mode 100644 Morphic.Client/MessageWatcherNativeWindow.cs delete mode 100644 Morphic.Client/Morphic.Client.csproj delete mode 100644 Morphic.Client/Properties/Resources.Designer.cs delete mode 100644 Morphic.Client/Properties/Resources.resx delete mode 100644 Morphic.Client/Properties/launchSettings.json delete mode 100644 Morphic.Client/Solutions/jaws2019.solutions.json delete mode 100644 Morphic.Client/Solutions/jaws2020.solutions.json delete mode 100644 Morphic.Client/Solutions/windows.solutions.json delete mode 100644 Morphic.Client/StringDictionarySetting.cs delete mode 100644 Morphic.Client/UserSettings.Designer.cs delete mode 100644 Morphic.Client/UserSettings.settings delete mode 100644 Morphic.Client/WinApi.cs delete mode 100644 Morphic.Client/WindowsUserSettings.cs delete mode 100644 Morphic.Client/app.manifest delete mode 100644 Morphic.Client/appsettings.Debug.json delete mode 100644 Morphic.Client/appsettings.Development.json delete mode 100644 Morphic.Client/appsettings.Production.json delete mode 100644 Morphic.Client/appsettings.Staging.json delete mode 100644 Morphic.Client/build-info.json delete mode 100644 Morphic.Client/quickstrip.json delete mode 100644 Morphic.Client/solutions.json5 delete mode 100644 Morphic.Client/test-bar.json5 delete mode 100644 Morphic.Core.Tests/JsonExtensionTests.cs delete mode 100644 Morphic.Core.Tests/KeychainTests.cs delete mode 100644 Morphic.Core.Tests/Morphic.Core.Tests.csproj delete mode 100644 Morphic.Core.Tests/PreferencesTests.cs delete mode 100644 Morphic.Core.Tests/StorageTests.cs delete mode 100644 Morphic.Core.Tests/UserTests.cs delete mode 100644 Morphic.Core/Community/BarItem.cs delete mode 100644 Morphic.Core/Community/Community.cs delete mode 100644 Morphic.Core/Community/UserBar.cs delete mode 100644 Morphic.Core/ICredentials.cs delete mode 100644 Morphic.Core/IDataProtection.cs delete mode 100644 Morphic.Core/IRecord.cs delete mode 100644 Morphic.Core/IUserSettings.cs delete mode 100644 Morphic.Core/JsonExtensions.cs delete mode 100644 Morphic.Core/KeyCredentials.cs delete mode 100644 Morphic.Core/Keychain.cs delete mode 100644 Morphic.Core/Preferences.cs delete mode 100644 Morphic.Core/Storage.cs delete mode 100644 Morphic.Core/TypeConversion.cs delete mode 100644 Morphic.Core/User.cs delete mode 100644 Morphic.Core/UsernameCredentials.cs delete mode 100644 Morphic.ManualTester/App.xaml delete mode 100644 Morphic.ManualTester/App.xaml.cs delete mode 100644 Morphic.ManualTester/AssemblyInfo.cs delete mode 100644 Morphic.ManualTester/Hourglass.png delete mode 100644 Morphic.ManualTester/Icon.ico delete mode 100644 Morphic.ManualTester/Icon.png delete mode 100644 Morphic.ManualTester/MainWindow.xaml delete mode 100644 Morphic.ManualTester/MainWindow.xaml.cs delete mode 100644 Morphic.ManualTester/ManualControlBoolean.xaml delete mode 100644 Morphic.ManualTester/ManualControlBoolean.xaml.cs delete mode 100644 Morphic.ManualTester/ManualControlDouble.xaml delete mode 100644 Morphic.ManualTester/ManualControlDouble.xaml.cs delete mode 100644 Morphic.ManualTester/ManualControlInteger.xaml delete mode 100644 Morphic.ManualTester/ManualControlInteger.xaml.cs delete mode 100644 Morphic.ManualTester/ManualControlString.xaml delete mode 100644 Morphic.ManualTester/ManualControlString.xaml.cs delete mode 100644 Morphic.ManualTester/Morphic.ManualTester.csproj delete mode 100644 Morphic.ManualTester/Properties/PublishProfiles/Standalone.pubxml delete mode 100644 Morphic.ManualTester/Properties/PublishProfiles/Standalone.pubxml.user delete mode 100644 Morphic.ManualTester/SolutionHeader.xaml delete mode 100644 Morphic.ManualTester/SolutionHeader.xaml.cs delete mode 100644 Morphic.ManualTesterCLI/Morphic.ManualTesterCLI.csproj delete mode 100644 Morphic.ManualTesterCLI/Program.cs delete mode 100644 Morphic.ManualTesterCLI/Properties/PublishProfiles/Standalone.pubxml delete mode 100644 Morphic.ManualTesterCLI/Properties/PublishProfiles/Standalone.pubxml.user delete mode 100644 Morphic.ManualTesterCLI/Properties/launchSettings.json delete mode 100644 Morphic.ManualTesterCLI/RegistryManager.cs delete mode 100644 Morphic.Service/AuthService.cs delete mode 100644 Morphic.Service/HttpRequestMessageExtensions.cs delete mode 100644 Morphic.Service/HttpResponseMessageExtensions.cs delete mode 100644 Morphic.Service/HttpService.cs delete mode 100644 Morphic.Service/IHttpServiceCredentialsProvider.cs delete mode 100644 Morphic.Service/Morphic.Service.csproj delete mode 100644 Morphic.Service/MorphicSession.cs delete mode 100644 Morphic.Service/PreferencesService.cs delete mode 100644 Morphic.Service/Session.cs delete mode 100644 Morphic.Service/UserService.cs delete mode 100644 Morphic.Settings.Tests/Morphic.Settings.Tests.csproj delete mode 100644 Morphic.Settings.Tests/Resolvers/ResolverTests.cs delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Display/DisplaySettingsHandlerTests.cs delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/IniFileSettingsHandlerTests.cs delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/IniFileTests.cs delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/handler-test.ini delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/read-test.ini delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/write-test.expect.ini delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Ini/write-test.ini delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/Registry/RegistrySettingsHandlerTests.cs delete mode 100644 Morphic.Settings.Tests/SettingsHandlers/test-solutions.json5 delete mode 100644 Morphic.Settings.Tests/SolutionsRegistry/SolutionsTests.cs delete mode 100644 Morphic.Settings.Tests/TestUtil.cs delete mode 100644 Morphic.Settings/Morphic.Settings.csproj delete mode 100644 Morphic.Settings/Resolvers/EnvironmentResolver.cs delete mode 100644 Morphic.Settings/Resolvers/FolderResolver.cs delete mode 100644 Morphic.Settings/Resolvers/RegistryResolver.cs delete mode 100644 Morphic.Settings/Resolvers/Resolver.cs delete mode 100644 Morphic.Settings/Resolvers/ResolvingString.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Display/DisplayHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/FixedSettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/IniFile.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/IniFileReader.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/IniFileRegex.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/IniFileWriter.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/IniSettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Ini/README.md delete mode 100644 Morphic.Settings/SettingsHandlers/Process/ProcessSettingGroup.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Process/ProcessSettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Registry/RegistrySettingGroup.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Registry/RegistrySettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Setting.cs delete mode 100644 Morphic.Settings/SettingsHandlers/SettingGroup.cs delete mode 100644 Morphic.Settings/SettingsHandlers/SettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/SystemSettings/ISystemSettingItem.cs delete mode 100644 Morphic.Settings/SettingsHandlers/SystemSettings/SystemSettingItem.cs delete mode 100644 Morphic.Settings/SettingsHandlers/SystemSettings/SystemSettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Theme/ThemeSettingGroup.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Theme/ThemeSettingsHandler.cs delete mode 100644 Morphic.Settings/SettingsHandlers/Values.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/JsonHelpers.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/SettingId.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/Solution.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/SolutionServices.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/Solutions.cs delete mode 100644 Morphic.Settings/SolutionsRegistry/TypeResolver.cs delete mode 100644 Morphic.Setup/License.rtf delete mode 100644 Morphic.Setup/Morphic.Setup.wixproj delete mode 100644 Morphic.Setup/Product.wxs delete mode 100644 Morphic.Windows.Native/Audio/AudioEndpoint.cs delete mode 100644 Morphic.Windows.Native/Display/Display.cs delete mode 100644 Morphic.Windows.Native/Input/Keyboard.cs delete mode 100644 Morphic.Windows.Native/Input/Mouse.cs delete mode 100644 Morphic.Windows.Native/Morphic.Windows.Native.csproj delete mode 100644 Morphic.Windows.Native/Processor/Processor.cs delete mode 100644 Morphic.Windows.Native/Speech/SelectionReader.cs delete mode 100644 Morphic.Windows.Native/Spi.cs delete mode 100644 Morphic.Windows.Native/WindowsApi.cs delete mode 100644 Morphic.Windows.Native/WindowsCom/Appx.cs delete mode 100644 Morphic.Windows.Native/WindowsCom/CLSCTX.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/EDataFlow.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/ERole.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/Exceptions/NoDeviceIsAvailableException.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/IAudioEndpointVolume.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/IMMDevice.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/IMMDeviceEnumerator.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/MMDevice.cs delete mode 100644 Morphic.Windows.Native/WindowsCoreAudio/MMDeviceEnumerator.cs delete mode 100644 Morphic.Windows.Native/WindowsSession/WindowsSession.cs delete mode 100644 MorphicWin.sln delete mode 100644 README.md delete mode 100644 azure-pipelines.yml delete mode 100644 documentation/configuration.md delete mode 100644 documentation/solutions.md delete mode 100644 set-build-info.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index eb0527ab..00000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.vs/ -*.csproj.user -bin/ -obj/ -Morphic.*Setup/Generated.wxs -Morphic.Client/appsettings.Local.json -Morphic.Client/appsettings.json -Morphic.Client/BuildVersion.txt -Morphic.Bar diff --git a/LICENSE.txt b/LICENSE.txt index d19159d7..e948c92f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,10 +1,34 @@ -Copyright 2020 Raising the Floor - International +BSD 3-Clause License + +Copyright 2020-2021, Raising the Floor - US +All rights reserved. + +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. -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 @@ -19,4 +43,4 @@ 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 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 4079828e..00000000 --- a/Morphic.Client/App.xaml.cs +++ /dev/null @@ -1,935 +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 Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - 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!; - - 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; } - } - - 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; } - } - - private struct CommonConfigurationContents - { - public ConfigurableFeatures.AutorunConfigOption? AutorunConfig; - public bool CheckForUpdatesIsEnabled; - public bool CloudSettingsTransferIsEnabled; - public bool ResetSettingsIsEnabled; - public ConfigurableFeatures.MorphicBarVisibilityAfterLoginOption? MorphicBarVisibilityAfterLogin; - public List ExtraMorphicBarItems; - } - 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 == 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 != 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 != null) - { - result.CheckForUpdatesIsEnabled = deserializedJson.features.checkForUpdates.enabled.Value; - } - - // capture the cloud settings transfer "is enabled" setting - if (deserializedJson.features?.cloudSettingsTransfer?.enabled != null) - { - result.CloudSettingsTransferIsEnabled = deserializedJson.features.cloudSettingsTransfer.enabled.Value; - } - - // capture the reset settings (to standard) "is enabled" setting - if (deserializedJson.features?.resetSettings?.enabled != 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 != 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; - - // if the item is invalid, log the error and skip this item - if ((extraItemType == null) || (extraItemLabel == null) || (extraItemTooltipHeader == 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 == 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 == 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; - } - - var extraMorphicBarItem = new MorphicBarExtraItem(); - extraMorphicBarItem.type = extraItemType; - extraMorphicBarItem.label = extraItemLabel; - extraMorphicBarItem.tooltipHeader = extraItemTooltipHeader; - extraMorphicBarItem.tooltipText = extraItemTooltipText; - extraMorphicBarItem.url = extraItemUrl; - extraMorphicBarItem.function = extraItemFunction; - result.ExtraMorphicBarItems.Add(extraMorphicBarItem); - } - } - - return result; - } - - /// - /// 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.AddTransient(); - services.AddSingleton(); - services.AddTransient(); - services.AddSingleton(s => BarPresets.Default); - services.AddSolutionsRegistryServices(); - services.AddSingleton(s => Solutions.FromFile(s, AppPaths.GetAppFile("solutions.json5"))); - } - - private async Task ConfigureCountlyAsync() - { - // 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 telemetryDeviceUuid = AppOptions.TelemetryDeviceUuid; - if (telemetryDeviceUuid == null) - { - telemetryDeviceUuid = "D_" + Guid.NewGuid().ToString(); - AppOptions.TelemetryDeviceUuid = telemetryDeviceUuid; - } - - IConfigurationSection? section = this.Configuration.GetSection("Countly"); - CountlyConfig cc = new CountlyConfig - { - serverUrl = section["ServerUrl"], - appKey = section["AppKey"], - appVersion = BuildInfo.Current.InformationalVersion, - developerProvidedDeviceId = telemetryDeviceUuid, - }; - - await Countly.Instance.Init(cc); - await Countly.Instance.SessionBegin(); - CountlyBase.IsLoggingEnabled = true; - } - - 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); - - MessageBox.Show($"Morphic ran into a problem:\n\n{e.Exception.Message}\n\nFurther information:\n{e.Exception}", "Morphic", MessageBoxButton.OK, MessageBoxImage.Warning); - - // 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(); - } - - protected override async void OnStartup(StartupEventArgs e) - { - this.Dispatcher.UnhandledException += this.App_DispatcherUnhandledException; - - this.Configuration = this.GetConfiguration(); - ServiceCollection collection = new ServiceCollection(); - this.ConfigureServices(collection); - this.ServiceProvider = collection.BuildServiceProvider(); - - base.OnStartup(e); - this.Logger = this.ServiceProvider.GetRequiredService>(); - - // 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, - morphicBarvisibilityAfterLogin: commonConfiguration.MorphicBarVisibilityAfterLogin, - morphicBarExtraItems: commonConfiguration.ExtraMorphicBarItems - ); - - this.MorphicSession = this.ServiceProvider.GetRequiredService(); - this.MorphicSession.UserChangedAsync += this.Session_UserChangedAsync; - - this.Logger.LogInformation("App Started"); - - this.morphicMenu = new MorphicMenu(); - - this.RegisterGlobalHotKeys(); - await this.ConfigureCountlyAsync(); - - 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) - { - Registry.SetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes", - "LastHighContrastTheme", @"%SystemRoot\resources\Ease of Access Themes\hcwhite.theme", - RegistryValueKind.ExpandString); - - // For windows 10 1809+ - Registry.SetValue(@"HKEY_CURRENT_USER\Control Panel\Accessibility\HighContrast", - "High Contrast Scheme", "High Contrast White"); - } - } - - 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 != null) - { - // get the adapterId and sourceId for this monitor - var adapterIdAndSourceId = Morphic.Windows.Native.Display.Display.GetAdapterIdAndSourceId(monitorName); - if (adapterIdAndSourceId != null) - { - // get the current DPI offset - var currentDisplayDpiOffset = Morphic.Windows.Native.Display.Display.GetCurrentDpiOffsetAndRange(adapterIdAndSourceId.Value.adapterId, adapterIdAndSourceId.Value.sourceId); - if (currentDisplayDpiOffset != 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 - if (await this.MorphicSession.GetSetting(SettingId.LightThemeSystem) != !darkModeEnabledDefault) - { - await this.MorphicSession.SetSetting(SettingId.LightThemeSystem, !darkModeEnabledDefault); - await this.MorphicSession.SetSetting(SettingId.LightThemeApps, !darkModeEnabledDefault); - } - } - - private async Task Session_UserChangedAsync(object? sender, EventArgs e) - { - if (sender is MorphicSession morphicSession) - { - if (morphicSession.SignedIn) - { - var lastCommunityId = AppOptions.Current.LastCommunity; - if (lastCommunityId != null) - { - // if the user previously selected a community bar, show that one now - await this.BarManager.LoadSessionBarAsync(morphicSession, lastCommunityId); - } - 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; - - // 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; - for (int i = 0; i < changeMorphicBarMenuItems.Count; 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 newMenuItem = new MenuItem(); - newMenuItem.Header = community.Name; - newMenuItem.Tag = community.Id; - if (community.Id == AppOptions.Current.LastCommunity) - { - newMenuItem.IsChecked = true; - addedCheckmarkByCurrentCommunityBar = true; - } - newMenuItem.Click += CustomMorphicBarMenuItem_Click; - // - this.morphicMenu.ChangeMorphicBar.Items.Insert(iCommunity, newMenuItem); - } - - // if no custom bar was checked, check the community 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 = (string)senderAsMenuItem.Tag; - - await this.BarManager.LoadSessionBarAsync(this.MorphicSession, communityId); - } - - private void RegisterGlobalHotKeys() - { - HotkeyManager.Current.AddOrReplace("Login with Morphic", Key.M, ModifierKeys.Control | ModifierKeys.Shift, (sender, e) => - { - this.Dialogs.OpenDialog(); - }); - HotkeyManager.Current.AddOrReplace("Show Morphic", Key.M, ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Alt, (sender, e) => - { - this.BarManager.ShowBar(); - }); - } - - 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 == 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(); - await Countly.Instance.SessionEnd(); - - 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/amazon-brands.svg b/Morphic.Client/Assets/bar-icons/amazon-brands.svg deleted file mode 100644 index e9ff652e..00000000 --- a/Morphic.Client/Assets/bar-icons/amazon-brands.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/calendar-solid.svg b/Morphic.Client/Assets/bar-icons/calendar-solid.svg deleted file mode 100644 index 1e6bd8ce..00000000 --- a/Morphic.Client/Assets/bar-icons/calendar-solid.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/camera-solid.svg b/Morphic.Client/Assets/bar-icons/camera-solid.svg deleted file mode 100644 index f65c158d..00000000 --- a/Morphic.Client/Assets/bar-icons/camera-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/comment-solid.svg b/Morphic.Client/Assets/bar-icons/comment-solid.svg deleted file mode 100644 index d8e3c5db..00000000 --- a/Morphic.Client/Assets/bar-icons/comment-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope-open-text.svg b/Morphic.Client/Assets/bar-icons/envelope-open-text.svg deleted file mode 100644 index 821168c0..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-open-text.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope-open.svg b/Morphic.Client/Assets/bar-icons/envelope-open.svg deleted file mode 100644 index 947ae25a..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-open.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope-outline-open.svg b/Morphic.Client/Assets/bar-icons/envelope-outline-open.svg deleted file mode 100644 index 0b71db8f..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-outline-open.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope-outline.svg b/Morphic.Client/Assets/bar-icons/envelope-outline.svg deleted file mode 100644 index a2557ef2..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-outline.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope-solid.svg b/Morphic.Client/Assets/bar-icons/envelope-solid.svg deleted file mode 100644 index 1473865a..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/envelope.svg b/Morphic.Client/Assets/bar-icons/envelope.svg deleted file mode 100644 index edbcad3d..00000000 --- a/Morphic.Client/Assets/bar-icons/envelope.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/facebook.svg b/Morphic.Client/Assets/bar-icons/facebook.svg deleted file mode 100644 index 67dec276..00000000 --- a/Morphic.Client/Assets/bar-icons/facebook.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/gmail.svg b/Morphic.Client/Assets/bar-icons/gmail.svg deleted file mode 100644 index 2898d0ef..00000000 --- a/Morphic.Client/Assets/bar-icons/gmail.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/google-brands.svg b/Morphic.Client/Assets/bar-icons/google-brands.svg deleted file mode 100644 index 6937fe11..00000000 --- a/Morphic.Client/Assets/bar-icons/google-brands.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/google-drive-brands.svg b/Morphic.Client/Assets/bar-icons/google-drive-brands.svg deleted file mode 100644 index fbc456f7..00000000 --- a/Morphic.Client/Assets/bar-icons/google-drive-brands.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/images-solid.svg b/Morphic.Client/Assets/bar-icons/images-solid.svg deleted file mode 100644 index 6c02f470..00000000 --- a/Morphic.Client/Assets/bar-icons/images-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/instagram.svg b/Morphic.Client/Assets/bar-icons/instagram.svg deleted file mode 100644 index e9c09fdf..00000000 --- a/Morphic.Client/Assets/bar-icons/instagram.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/link-solid.svg b/Morphic.Client/Assets/bar-icons/link-solid.svg deleted file mode 100644 index ef99977b..00000000 --- a/Morphic.Client/Assets/bar-icons/link-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/minus.svg b/Morphic.Client/Assets/bar-icons/minus.svg deleted file mode 100644 index 3c9e3861..00000000 --- a/Morphic.Client/Assets/bar-icons/minus.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/morphic-logo.svg b/Morphic.Client/Assets/bar-icons/morphic-logo.svg deleted file mode 100644 index e573ae47..00000000 --- a/Morphic.Client/Assets/bar-icons/morphic-logo.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/music-solid.svg b/Morphic.Client/Assets/bar-icons/music-solid.svg deleted file mode 100644 index 3289b3a7..00000000 --- a/Morphic.Client/Assets/bar-icons/music-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/newspaper-solid.svg b/Morphic.Client/Assets/bar-icons/newspaper-solid.svg deleted file mode 100644 index 3fc9afa5..00000000 --- a/Morphic.Client/Assets/bar-icons/newspaper-solid.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/outlook.svg b/Morphic.Client/Assets/bar-icons/outlook.svg deleted file mode 100644 index 5592b366..00000000 --- a/Morphic.Client/Assets/bar-icons/outlook.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/paypal.svg b/Morphic.Client/Assets/bar-icons/paypal.svg deleted file mode 100644 index d41a85a6..00000000 --- a/Morphic.Client/Assets/bar-icons/paypal.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/plus.svg b/Morphic.Client/Assets/bar-icons/plus.svg deleted file mode 100644 index e8681531..00000000 --- a/Morphic.Client/Assets/bar-icons/plus.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/question-solid.svg b/Morphic.Client/Assets/bar-icons/question-solid.svg deleted file mode 100644 index 2a3a5c66..00000000 --- a/Morphic.Client/Assets/bar-icons/question-solid.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/reddit.svg b/Morphic.Client/Assets/bar-icons/reddit.svg deleted file mode 100644 index ac7a0fb0..00000000 --- a/Morphic.Client/Assets/bar-icons/reddit.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/shopping-cart-solid.svg b/Morphic.Client/Assets/bar-icons/shopping-cart-solid.svg deleted file mode 100644 index 155dcd30..00000000 --- a/Morphic.Client/Assets/bar-icons/shopping-cart-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/skype-brands.svg b/Morphic.Client/Assets/bar-icons/skype-brands.svg deleted file mode 100644 index 75554c29..00000000 --- a/Morphic.Client/Assets/bar-icons/skype-brands.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/skype.svg b/Morphic.Client/Assets/bar-icons/skype.svg deleted file mode 100644 index ab315dfd..00000000 --- a/Morphic.Client/Assets/bar-icons/skype.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/telegram.svg b/Morphic.Client/Assets/bar-icons/telegram.svg deleted file mode 100644 index 22cd21d7..00000000 --- a/Morphic.Client/Assets/bar-icons/telegram.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/test/grid.svg b/Morphic.Client/Assets/bar-icons/test/grid.svg deleted file mode 100644 index b482016f..00000000 --- a/Morphic.Client/Assets/bar-icons/test/grid.svg +++ /dev/null @@ -1,89 +0,0 @@ - - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon image/svg+xml - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/twitter.svg b/Morphic.Client/Assets/bar-icons/twitter.svg deleted file mode 100644 index 304965ff..00000000 --- a/Morphic.Client/Assets/bar-icons/twitter.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/viber.svg b/Morphic.Client/Assets/bar-icons/viber.svg deleted file mode 100644 index f507a1e3..00000000 --- a/Morphic.Client/Assets/bar-icons/viber.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/video-solid.svg b/Morphic.Client/Assets/bar-icons/video-solid.svg deleted file mode 100644 index 6a7e1903..00000000 --- a/Morphic.Client/Assets/bar-icons/video-solid.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/whatsapp.svg b/Morphic.Client/Assets/bar-icons/whatsapp.svg deleted file mode 100644 index e6d6a3b9..00000000 --- a/Morphic.Client/Assets/bar-icons/whatsapp.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Morphic.Client/Assets/bar-icons/yahoo-mail.svg b/Morphic.Client/Assets/bar-icons/yahoo-mail.svg deleted file mode 100644 index 9e0e0200..00000000 --- a/Morphic.Client/Assets/bar-icons/yahoo-mail.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/bar-icons/youtube.svg b/Morphic.Client/Assets/bar-icons/youtube.svg deleted file mode 100644 index 65c3108f..00000000 --- a/Morphic.Client/Assets/bar-icons/youtube.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Morphic.Client/Assets/morphic-icon.ico b/Morphic.Client/Assets/morphic-icon.ico deleted file mode 100644 index 39f8ed229cfb47fbfe0b4ece0323875f7eb177d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160213 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 JPDHLkV1o4mne+ev diff --git a/Morphic.Client/Backups.cs b/Morphic.Client/Backups.cs deleted file mode 100644 index 22a73dcd..00000000 --- a/Morphic.Client/Backups.cs +++ /dev/null @@ -1,94 +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 == null) - { - preferences = new Preferences(); - await this.morphicSession.Solutions.CapturePreferences(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 55847987..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 != null) - { - System.Windows.Rect rc = window.GetRect(); - rc.Inflate(10, 10); - if (rc.Contains(cursor.Value)) - { - isOver = true; - if (this.mouseTimer == 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 aaa30767..00000000 --- a/Morphic.Client/Bar/BarImages.cs +++ /dev/null @@ -1,169 +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.Media; - using System.Windows.Media.Imaging; - using System.Xml; - using Config; - using SharpVectors.Converters; - using SharpVectors.Dom.Svg; - using SharpVectors.Renderers.Wpf; - - 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) - { - 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 = { "", ".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif" }; - - string? foundFile = extensions.Select(extension => assetFile + extension) - .FirstOrDefault(File.Exists); - - return foundFile; - } - - /// - /// 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 an SVG image. - ImageSource? TrySvg() - { - try - { - using FileSvgReader svg = new FileSvgReader(new WpfDrawingSettings()); - DrawingGroup drawingGroup = svg.Read(imagePath); - if (color.HasValue) - { - ChangeDrawingColor(drawingGroup, color.Value); - } - - return new DrawingImage(drawingGroup); - } - catch (Exception e) when (e is NotSupportedException || e is XmlException || e is SvgException) - { - return null; - } - } - - // 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 || e is SvgException) - { - return null; - } - } - - if (!imagePath.Contains('/')) - { - imagePath = GetBarIconFile(imagePath) ?? imagePath; - } - - if (Path.GetExtension(imagePath) == ".svg") - { - result = TrySvg() ?? TryBitmap(); - } - else - { - result = TryBitmap() ?? TrySvg(); - } - - 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 89ad11ee..00000000 --- a/Morphic.Client/Bar/BarManager.cs +++ /dev/null @@ -1,362 +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 != 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 != 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 != 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; - 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 != 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 != null) - { - this.CloseBar(); - } - - this.BarIsLoaded = true; - - if (bar != null) - { - 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) - { - if (this.firstBar && AppOptions.Current.Launch.BarFile != 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 != null) - { - community = session.Communities.FirstOrDefault(c => c.Id == communityId); - } - - //if (community == 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 != null) - // { - // userBar = await bars[community.Id]; - // } - //} - //} - - if (community != null) - { - userBar ??= await session.GetBar(community.Id); - - 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 != null) - { - barData.CommunityId = community.Id; - } - - AppOptions.Current.LastCommunity = community?.Id; - } - else - { - // if the community could not be found, show the Basic MorphicBar instead - this.LoadBasicMorphicBar(); - - AppOptions.Current.LastCommunity = null; - } - - AppOptions.Current.Communities = session.Communities.Select(c => c.Id).ToArray(); - } - - /// - /// 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 5f31b030..00000000 --- a/Morphic.Client/Bar/Data/Actions/ApplicationAction.cs +++ /dev/null @@ -1,356 +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 != null) - { - return Imaging.CreateBitmapSourceFromHIcon( - System.Drawing.Icon.ExtractAssociatedIcon(this.AppPath).Handle, - Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); - } - else - { - return null; - } - } - } - - /// - /// Start a default application. This value points to an entry in default-apps.json5. - /// - [JsonProperty("default")] - public string? DefaultAppName { get; set; } - - public BarAction? DefaultApp { get; private 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; - - - /// - /// Executable name, or the full path to it. If also providing arguments, surround the executable path with quotes. - /// - [JsonProperty("exe", Required = Required.Always)] - public string ExeName - { - get => this.exeNameValue ?? string.Empty; - set - { - this.exeNameValue = value; - - // A url like "mailto:" - bool isUrl = this.exeNameValue.Length > 3 && this.exeNameValue.EndsWith(':'); - if (isUrl) - { - this.Shell = true; - } - - if (this.exeNameValue.StartsWith("appx:", StringComparison.InvariantCultureIgnoreCase)) - { - this.AppX = true; - this.exeNameValue = this.exeNameValue.Substring(5); - } - - if (this.Shell || this.AppX || this.exeNameValue.Length == 0) - { - this.AppPath = null; - } - else - { - if (this.ExeName.StartsWith('"')) - { - int nextQuote = this.exeNameValue.IndexOf('"', 1); - if (nextQuote < 0) - { - App.Current.Logger.LogWarning($"Executable path [{this.ExeName}] has mismatching quote"); - this.AppPath = this.ExeName.Substring(1); - } - else - { - this.AppPath = this.ExeName.Substring(1, nextQuote - 1); - this.ArgumentsString = this.ExeName.Substring(nextQuote + 1).Trim(); - } - } - - this.AppPath = this.ResolveAppPath(this.exeNameValue); - App.Current.Logger.LogDebug($"Resolved exe file '{this.exeNameValue}' to '{this.AppPath ?? "(null)"}'"); - } - - this.IsAvailable = this.AppPath != 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(); - - /// - /// Resolves the path of an executable, by looking in the "App Paths" registry key or the PATH environment. - /// If a full path is provided, and it doesn't exist, then the path for the file name alone is resolved. - /// - /// Environment variables in the file path are also resolved. - /// - /// The `exeName` input value. - /// Full path to the executable if found, or null. - private string? ResolveAppPath(string exeName) - { - string file = Environment.ExpandEnvironmentVariables(exeName); - string ext = Path.GetExtension(file).ToLower(); - string withExe, withoutExe; - - // Try with the .exe extension first, then without, but if the file ends with a '.', then try that first. - // (similar to CreateProcess) - if (file.EndsWith(".")) - { - string? result1 = this.ResolveAppPathAsIs(file); - if (result1 != null) - { - return result1; - } - - withExe = Path.ChangeExtension(file, ".exe"); - withoutExe = Path.ChangeExtension(file, null); - } - else if (ext == ".exe") - { - withExe = file; - withoutExe = Path.ChangeExtension(file, null); - } - else - { - withExe = file + ".exe"; - withoutExe = file; - } - - string? result = this.ResolveAppPathAsIs(withExe); - if (result != null) - { - return result; - } - else - { - return this.ResolveAppPathAsIs(withoutExe); - } - } - - /// - /// Called by ResolveAppPath to perform the actual resolving work. - /// - /// - /// Full path to the executable if found, or null. - private string? ResolveAppPathAsIs(string file) - { - string? fullPath = null; - - if (Path.IsPathRooted(file)) - { - if (File.Exists(file)) - { - fullPath = file; - } - - file = Path.GetFileName(file); - } - - return fullPath ?? this.SearchAppPaths(file) ?? this.SearchPathEnv(file); - } - - /// - /// Searches the directories in the PATH environment variable. - /// - /// - /// null if not found. - private string? SearchPathEnv(string file) - { - // 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 string? SearchAppPaths(string file) - { - string? fullPath = null; - - // Look in *\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths - foreach (RegistryKey rootKey in new[] {Registry.CurrentUser, Registry.LocalMachine}) - { - RegistryKey? key = - rootKey.OpenSubKey($@"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\{file}"); - if (key != null) - { - fullPath = key.GetValue(string.Empty) as string; - if (fullPath != null) - { - break; - } - } - } - - return fullPath; - } - - protected override Task InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - if (this.DefaultApp != null && string.IsNullOrEmpty(this.ExeName)) - { - return this.DefaultApp.InvokeAsync(source); - } - - if (this.AppX) - { - var pid = Appx.Start(this.ExeName); - return Task.FromResult(pid > 0 ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult); - } - - if (!this.NewInstance && (Keyboard.Modifiers & ModifierKeys.Shift) != ModifierKeys.Shift) - { - bool activated = this.ActivateInstance().IsSuccess; - if (activated) - { - return Task.FromResult(IMorphicResult.SuccessResult); - } - } - - ProcessStartInfo startInfo = new ProcessStartInfo() - { - FileName = this.AppPath ?? this.ExeName, - ErrorDialog = true, - // This is required to start taskmgr (the UAC prompt) - UseShellExecute = true, - WindowStyle = this.WindowStyle - - }; - - if (this.Shell) - { - startInfo.UseShellExecute = true; - } - - if (this.Arguments.Count > 0) - { - foreach (string argument in this.Arguments) - { - startInfo.ArgumentList.Add(this.ResolveString(argument, source)); - } - } - else - { - startInfo.Arguments = this.ResolveString(this.ArgumentsString, source); - } - - foreach (var (key, value) in this.EnvironmentVariables) - { - startInfo.EnvironmentVariables.Add(key, this.ResolveString(value, source)); - } - - Process? process = Process.Start(startInfo); - - return Task.FromResult(process != null ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult); - } - - /// - /// Activates a running instance of the application. - /// - /// false if it could not be done. - /// - private IMorphicResult 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 ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult; - } - } -} diff --git a/Morphic.Client/Bar/Data/Actions/BarAction.cs b/Morphic.Client/Bar/Data/Actions/BarAction.cs deleted file mode 100644 index f5b725d4..00000000 --- a/Morphic.Client/Bar/Data/Actions/BarAction.cs +++ /dev/null @@ -1,342 +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) - { - IMorphicResult 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 != null) - { - MessageBox.Show($"There was a problem performing the action:\n\n{e.UserMessage}", - "Custom MorphicBar", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); - } - - result = IMorphicResult.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 "magnify": - { - if (source == "on") - { - await Countly.RecordEvent("magnifierShow"); - } - else if (source == "off") - { - await Countly.RecordEvent("magnifierHide"); - } - } - break; - case "read-aloud": - { - if (source == "play") - { - await Countly.RecordEvent("readSelectedPlay"); - } - else if (source == "stop") - { - await Countly.RecordEvent("readSelectedStop"); - break; - } - } - break; - case "": - switch (source) - { - case "com.microsoft.windows.colorFilters/enabled": - { - if (toggleState == true) - { - await Countly.RecordEvent("colorFiltersOn"); - return; - } - else - { - await Countly.RecordEvent("colorFiltersOff"); - return; - } - } - break; - case "com.microsoft.windows.highContrast/enabled": - { - if (toggleState == true) - { - await Countly.RecordEvent("highContrastOn"); - return; - } - else - { - await Countly.RecordEvent("highContrastOff"); - return; - } - } - break; - case "com.microsoft.windows.nightMode/enabled": - { - if (toggleState == true) - { - await Countly.RecordEvent("nightModeOn"); - return; - } - else - { - await Countly.RecordEvent("nightModeOff"); - return; - } - } - break; - case "copy": - { - await Countly.RecordEvent("screenSnip"); - } - break; - case "dark-mode": - { - if (toggleState == true) - { - await Countly.RecordEvent("darkModeOn"); - } - else - { - await Countly.RecordEvent("darkModeOff"); - } - } - 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; - 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) - { - return Task.FromResult(IMorphicResult.SuccessResult); - } - } - - [JsonTypeName("internal")] - public class InternalAction : BarAction - { - [JsonProperty("function", Required = Required.Always)] - public string? FunctionName { get; set; } - - [JsonProperty("args")] - public Dictionary Arguments { get; set; } = new Dictionary(); - - public string? TelemetryEventName { get; set; } - - protected override Task InvokeAsyncImpl(string? source = null, bool? toggleState = null) - { - try - { - if (this.FunctionName == null) - { - return Task.FromResult(IMorphicResult.SuccessResult); - } - - Dictionary resolvedArgs = this.Arguments - .ToDictionary(kv => kv.Key, kv => this.ResolveString(kv.Value, source) ?? string.Empty); - - resolvedArgs.Add("state", toggleState == true ? "on" : "off"); - - return InternalFunctions.Default.InvokeFunction(this.FunctionName, resolvedArgs); - } - finally - { - if (this.TelemetryEventName != null) - { - Countly.RecordEvent(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 IMorphicResult.SuccessResult; - } - } - - [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 != null; - } - - return Task.FromResult(success ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult); - } - - 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 bfebc993..00000000 --- a/Morphic.Client/Bar/Data/Actions/Functions.cs +++ /dev/null @@ -1,251 +0,0 @@ -namespace Morphic.Client.Bar.Data.Actions -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.IO; - using System.Linq; - using System.Media; - using System.Runtime.InteropServices; - using System.Threading.Tasks; - using System.Windows; - using Windows.Native.Input; - using Windows.Native.Speech; - using global::Windows.Media.SpeechSynthesis; - using Microsoft.Extensions.Logging; - using Settings.SettingsHandlers; - using Settings.SolutionsRegistry; - using UI; - using Clipboard = System.Windows.Forms.Clipboard; - using IDataObject = System.Windows.Forms.IDataObject; - - [HasInternalFunctions] - // ReSharper disable once UnusedType.Global - accessed via reflection. - public class Functions - { - [InternalFunction("screenshot")] - public static async Task Screenshot(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); - - // Hold down the windows key while pressing shift + s - const uint windowsKey = 0x5b; // VK_LWIN - Keyboard.PressKey(windowsKey, true); - System.Windows.Forms.SendKeys.SendWait("+s"); - Keyboard.PressKey(windowsKey, false); - - } - 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 true; - } - - [InternalFunction("menu", "key=Morphic")] - public static Task ShowMenu(FunctionArgs args) - { - // NOTE: this internal function is only called by the MorphicBar's Morphie menu button - App.Current.ShowMenuAsync(null, Morphic.Client.Menu.MorphicMenu.MenuOpenedSource.morphicBarIcon); - return Task.FromResult(true); - } - - /// - /// Lowers or raises the volume. - /// - /// direction: "up"/"down", amount: number of 1/100 to move - /// - [InternalFunction("volume", "direction", "amount=10")] - public static Task Volume(FunctionArgs args) - { - 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 Task.FromResult(true); - } - - // Plays the speech sound. - private static SoundPlayer? speechPlayer; - - /// - /// Reads the selected text. - /// - /// action: "play", "pause", or "stop" - /// - [InternalFunction("readAloud", "action")] - public static async Task ReadAloud(FunctionArgs args) - { - string action = args["action"]; - switch (action) - { - case "pause": - App.Current.Logger.LogError("ReadAloud: pause not supported"); - break; - - case "stop": - case "play": - Functions.speechPlayer?.Stop(); - Functions.speechPlayer?.Dispose(); - Functions.speechPlayer = null; - - if (action == "stop") - { - break; - } - - App.Current.Logger.LogDebug("ReadAloud: Storing clipboard"); - IDataObject? clipboardData = Clipboard.GetDataObject(); - Dictionary? dataStored = null; - if (clipboardData != null) - { - dataStored = clipboardData.GetFormats() - .ToDictionary(format => format, format => (object?)clipboardData.GetData(format, false)); - } - - Clipboard.Clear(); - - // Get the selection - App.Current.Logger.LogDebug("ReadAloud: Getting selected text"); - await SelectionReader.Default.GetSelectedText(System.Windows.Forms.SendKeys.SendWait); - string text = Clipboard.GetText(); - - // Restore the clipboard - App.Current.Logger.LogDebug("ReadAloud: Restoring clipboard"); - Clipboard.Clear(); - dataStored?.Where(kv => kv.Value != null).ToList() - .ForEach(kv => Clipboard.SetData(kv.Key, kv.Value)); - - // Talk the talk - SpeechSynthesizer synth = new SpeechSynthesizer(); - SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text); - speechPlayer = new SoundPlayer(stream.AsStream()); - speechPlayer.LoadCompleted += (o, args) => - { - speechPlayer.Play(); - }; - - speechPlayer.LoadAsync(); - - break; - - } - - return true; - } - - /// - /// Sends key strokes to the active application. - /// - /// keys: the keys (see MSDN for SendKeys.Send()) - /// - [InternalFunction("sendKeys", "keys")] - public static async Task SendKeys(FunctionArgs args) - { - await SelectionReader.Default.ActivateLastActiveWindow(); - System.Windows.Forms.SendKeys.SendWait(args["keys"]); - return true; - } - - [InternalFunction("signOut")] - public static async Task SignOut(FunctionArgs args) - { - var success = Morphic.Windows.Native.WindowsSession.WindowsSession.LogOff(); - return success; - } - - [InternalFunction("darkMode")] - public static async Task DarkMode(FunctionArgs args) - { - bool on = args["state"] == "on"; - - /* - * 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 - * - * 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(!on); - - // set apps dark/light theme - Setting appsThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeApps); - await appsThemeSetting.SetValueAsync(!on); - return true; - } - - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Windows API naming")] - [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Windows API naming")] - private static class WinApi - { - 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 5a1a382c..00000000 --- a/Morphic.Client/Bar/Data/Actions/InternalFunctions.cs +++ /dev/null @@ -1,183 +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 != null) - { - attr.SetFunction((InternalFunction)method.CreateDelegate(typeof(InternalFunction))); - yield return attr; - } - } - } - - /// - /// Invokes a built-in function. - /// - /// The function name. - /// The parameters. - /// - public Task InvokeFunction(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 == 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 ca1519ad..00000000 --- a/Morphic.Client/Bar/Data/Actions/SettingAction.cs +++ /dev/null @@ -1,64 +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 == null && !string.IsNullOrEmpty(source)) - { - setting = this.Solutions.GetSetting(source); - setting.SetValueAsync(toggleState); - } - else - { - setting = this.Setting; - } - - if (setting == null) - { - return Task.FromResult(IMorphicResult.SuccessResult); - } - - 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); - } - - return Task.FromResult(IMorphicResult.ErrorResult); - } - - 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 deb98729..00000000 --- a/Morphic.Client/Bar/Data/Actions/WebAction.cs +++ /dev/null @@ -1,89 +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; - 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 != 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 != null) - { - Process? process = Process.Start(new ProcessStartInfo() - { - FileName = this.ResolveString(this.Uri?.ToString(), source), - UseShellExecute = true - }); - success = process != null; - } - - return Task.FromResult(success ? IMorphicResult.SuccessResult : IMorphicResult.ErrorResult); - } - } -} diff --git a/Morphic.Client/Bar/Data/BarButton.cs b/Morphic.Client/Bar/Data/BarButton.cs deleted file mode 100644 index 9dbbcbab..00000000 --- a/Morphic.Client/Bar/Data/BarButton.cs +++ /dev/null @@ -1,242 +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.Media; - 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 == null || uri.IsFile) - { - localPath = BarImages.GetBarIconFile(this.imageValue); - if (localPath == null) - { - uri = new Uri(this.Bar.FrontEndUri, this.FrontendImagePath); - } - } - - if (localPath != null) - { - this.ImagePath = localPath; - } - else if (uri != null) - { - // Download later. - this.RemoteImage = uri; - } - } - } - } - - /// - /// The image to use. - /// - public ImageSource? ImageSource - { - get => this.imageSource; - set - { - this.imageSource = 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 != 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)) - { - this.ImageSource = BarImages.CreateImageSource(this.ImagePath); - success = this.ImageValue != null; - } - - // Fallback to a default image. - if (!success) - { - ImageSource? source = this.Action.DefaultImageSource; - if (source != null) - { - this.ImageSource = source; - success = true; - } - else - { - Uri? defaultUri = this.Action.DefaultImageUri; - if (defaultUri != 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 != 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 7e9f8149..00000000 --- a/Morphic.Client/Bar/Data/BarData.cs +++ /dev/null @@ -1,355 +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.IsPrimary && !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); - - /// - /// Gets the items for the additional buttons. - /// - public IEnumerable SecondaryItems => this.AllItems.Where(this.IsSecondaryItem) - .OrderByDescending(item => item.IsPrimary) - .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); - - // 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) - { - - BarButton extraBarItem = new BarButton(defaultBar); - extraBarItem.ToolTipHeader = extraItemData.tooltipHeader; - extraBarItem.ToolTip = extraItemData.tooltipText; - extraBarItem.Text = extraItemData.label ?? ""; - switch (extraItemData.type) - { - case "link": - extraBarItem.Action = new Morphic.Client.Bar.Data.Actions.WebAction(); - ((Morphic.Client.Bar.Data.Actions.WebAction)extraBarItem.Action).UrlString = extraItemData.url ?? ""; - break; - case "action": - 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!; - break; - default: - // unknown type; this should be an impossible code path - throw new NotImplementedException(); - } - //extraBarItem.ColorValue = "#00FF00"; - extraBarItem.IsPrimary = true; - // - defaultBar?.AllItems.Add(extraBarItem); - } - - // add a spacer entry - BarButton spacerBarItem = new BarButton(defaultBar); - spacerBarItem.ToolTipHeader = ""; - spacerBarItem.ToolTip = ""; - spacerBarItem.Text = ""; - spacerBarItem.ColorValue = "#FFFFFF"; - spacerBarItem.IsPrimary = true; - // - defaultBar?.AllItems.Add(spacerBarItem); - } - } - else - { - defaultBar = null; - } - - App.Current.Logger.LogInformation("Loading bar from {source}", barSource); - - BarData? bar; - - using (TextReader reader = content == 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); - - if (this.hasDeserialized) - { - // If a bar has no primary items, it looks stupid. Make all the items primary, and make them over-flow to - // the secondary bar. - bool hasPrimary = this.PrimaryItems.Any(item => !item.IsDefault); - if (!hasPrimary) - { - foreach (BarItem item in this.SecondaryItems) - { - item.IsPrimary = true; - } - - this.Overflow = BarOverflow.Secondary; - } - } - - 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 74bdee71..00000000 --- a/Morphic.Client/Bar/Data/BarItem.cs +++ /dev/null @@ -1,226 +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; } - - /// - /// 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; } = new NoOpAction(); - - /// - /// 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)) - { - if (ColorConverter.ConvertFromString(value) is Color color) - { - this.Color = color; - } - } - } - 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 767b6893..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 == null) - { - this.Hover.Background = this.LightenColor(this.Background ?? Colors.Transparent, 0.25f); - } - - if (force || this.Active.Background == null) - { - this.Active.Background = this.LightenColor(this.Background ?? Colors.Transparent, 0.5f); - } - - if (force || this.Focus.Background == 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 == 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 1678b0bd..00000000 --- a/Morphic.Client/Bar/Data/BarJson.cs +++ /dev/null @@ -1,378 +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 == null) - { - if (baseType.GetCustomAttributes().Any()) - { - // The type has already been resolved at the property. - type = baseType; - } - else - { - throw new JsonSerializationException( - $"Unable to get type of {baseType.Name} from '{this.typeFieldName} = ${name}'."); - } - } - - List ctorArgs = new List(); - bool gotCtor = false; - if (barData != null) - { - ctorArgs.Add(barData); - - // Find a constructor of (BarData, string) - ConstructorInfo? ctor = type.GetConstructor(new[] { barData.GetType(), typeof(string) }); - if (ctor != null) - { - // Get the property for the string argument. - ParameterInfo param = ctor.GetParameters().Last(); - JsonPropertyAttribute? attr = param.GetCustomAttribute(); - string? propertyName = attr?.PropertyName; - if (propertyName != 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() }) != null; - } - } - - object? instance = gotCtor - ? Activator.CreateInstance(type, ctorArgs.ToArray()) - : Activator.CreateInstance(type); - - if (instance == 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); - - // 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 != null && token.Type != JTokenType.Null) - { - Type? newType = this.GetNewType(jo, property); - object? value = newType == 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 == 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) != null; - } - - return jsonProperty; - } - } - -} diff --git a/Morphic.Client/Bar/Data/BarMultiButton.cs b/Morphic.Client/Bar/Data/BarMultiButton.cs deleted file mode 100644 index a2794b01..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; } = new NoOpAction(); - - [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 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 750686fc..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 != 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 1450befe..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 != 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 8b080d89..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 == 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 e06811ab..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 != 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 a8ef8066..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 == null) - ? target - : string.Format(format, target); - - MorphicMenuItem item = new MorphicMenuItem() - { - Header = finalName, - Open = finalTarget, - ParentMenuType = MorphicMenuItem.MenuType.contextMenu, - TelemetryType = telemetryType, - TelemetryCategory = (telemetryType != 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 fb753362..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 != 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 (Image image in this.GetAllChildren().OfType()) - { - image.SourceUpdated += this.ImageOnSourceUpdated; - this.ImageOnSourceUpdated(image, null); - } - } - - private void ImageOnSourceUpdated(object? sender, EventArgs? e) - { - if (sender is Image image) - { - if (image.Source 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 svg 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/ButtonBarControl.xaml b/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml deleted file mode 100644 index 144132e0..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml +++ /dev/null @@ -1,205 +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 393782a1..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ButtonBarControl.xaml.cs +++ /dev/null @@ -1,67 +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 == 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 void Button_OnClick(object sender, RoutedEventArgs e) - { - 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 fdb96574..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml +++ /dev/null @@ -1,47 +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 1c540a2b..00000000 --- a/Morphic.Client/Bar/UI/BarControls/ImageBarControl.xaml.cs +++ /dev/null @@ -1,34 +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; - - /// - /// 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) - { - 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 e3137f20..00000000 --- a/Morphic.Client/Bar/UI/BarControls/MultiButtonBarControl.xaml.cs +++ /dev/null @@ -1,462 +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.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 != 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 != null) - { - e.Handled = this.OpenContextMenu(sender, buttonInfo.Menu, buttonInfo.TelemetryCategory); - } - } - - private void Button_OnClick(object sender, RoutedEventArgs e) - { - BarMultiButton.ButtonInfo? buttonInfo = - this.Buttons.Where(b => b.Control == sender) - .Select(b => b.Button) - .FirstOrDefault(); - - if (buttonInfo != null) - { - MultiButtonBarControl.LastClickedControl = sender as Control; - MultiButtonBarControl.LastClickedTime = DateTime.Now; - - // Call the button action. - bool? state = (sender as ToggleButton)?.IsChecked; - buttonInfo.Action.InvokeAsync(buttonInfo.Value, state); - } - } - - /// 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 != 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 != 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": - Setting systemThemeSetting = App.Current.MorphicSession.Solutions.GetSetting(Settings.SolutionsRegistry.SettingId.LightThemeSystem); - // - var systemThemeIsLightTheme = await systemThemeSetting.GetValue(); - ((ToggleButton)this.Control).IsChecked = !systemThemeIsLightTheme; - // - // - systemThemeSetting.Changed += this.InverseSettingOnChanged; - this.Control.Unloaded += (sender, args) => systemThemeSetting.Changed -= this.InverseSettingOnChanged; - 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); - } - } - } - } - - /// - /// 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 05ed3eea..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 != 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 2baec065..00000000 --- a/Morphic.Client/Dialogs/Travel/CreateAccountPanel.xaml.cs +++ /dev/null @@ -1,324 +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 - - #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); - } - 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) - { - this.StepFrame.PushPanel(); - } - - #endregion - - public StepFrame StepFrame { get; set; } - } -} 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 f5160a8c..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 ca7f5f50..00000000 --- a/Morphic.Client/Dialogs/Travel/TravelWindow.xaml.cs +++ /dev/null @@ -1,78 +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 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 - { - - #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 - - 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(); - } - - } -} diff --git a/Morphic.Client/Icon.ico b/Morphic.Client/Icon.ico deleted file mode 100644 index 633cab33f17a5d137f8ffdf7693ce2866536e06e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43392 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

3j5s@zl zn(RaTd>Vti`?dAoMu%;XJI{W2hWEYiJkD94_((BJQ4+A815w(i1~Uu?-+SmgmW5rA z`p2I!0)w~);ws-LHP$|$3OP z@2Yl)Z$JOiLdH=DcHU6EEGKtPOD!{MwEdZ~jmzhv#DXH;SPH+uV_6#)9L&JrK|d`W zg3y(eL|I~ds$et-zl(QS zjUYVz@e;TRVCr%uvwQV7iT2$t&bCIM1h#NHaXUl|p((?`C@*p2+fr0){XVCL^XB`# z6Fb>24`Gv?(r0j$2=hzyC_~LlknI?|fXf4m^S|3X=pl#z>P#&6nPYq==m6m1lq-nl zQ=~-689D&fZM}pQ9<$#&ytgeFQ!s{`zNZ(n$~9v}itE+M&#*Dm=kxf0K1sYJ5H~15 zftVolEJi#uWDv&9@*?Dn&f}pyxaS|ZqeJs0b4QMt ziUpk(xn&62IWr*!*W;|>mQ+j0f_w=K9!dbzEr)`KeQEqz;+ZWGzFIwXX21JO8$Ka) zQVP^~E^H6iSd-HGb6tKP*Adk8OIj^~h{1#2NgWC*mx4UbhG1YbZg*L~FloL=zv}Lw z8UL%#dm_5^6Z^;6dg+BZsw;-sWwE|@m=o@P2j^jK1{XPPKjZ$y0&fjJe<_TiASNK}K7x3p zbgj>9!sTc3AhBjTd1YwGvJrm?Szel6_J0>p8Zjg`?NjdLi0Ji|f)ED2XqY6?gH~%$ z$@54Lngc!G&_ikC01~!K|GG1*XTGWJ8omC~A#NeAl+6-Fg7JvNZ2>0YGv}c|Fy&9P zwwvV8L*i*kBu_izFk8w{ zcZ^q`emz4HesY~b5j2z^>ecySRztlV7gj9!7S8C#JHuvMxPuYK{E{qr=UBD*C)b`i z5vpA{1obcG5Ac#T&{L)%;23;_yk6TLv;uLTmPlfe0?+4IQP)SJAk04MDa@^1{3Nlj zyEq}UMN!kn3#z;9l>@R8kE#Ji!B@{pO>?JFH@37g;2VZF6iz*ZI5)HEE}|dLxPRX{ zMT@r~qU6J$5$xzz+ok$7-|Jmp$dc1LI;ebjUbIqiKGni;At2^1m)B6dr+i^o+x8y~ zCm(Fq{$#Fit$=XHmxVJ@3*D%ZrTknYaf)j{&PaRQHk~hiiVu&U0X*AC@n{S%?(N$c zJ#VE3rrm@xN-vzRl?t>sz(e2Y$Ts2ZnuAiL;x&R~Fpn2j&% z|A6$@^y}aD-3|N95p*lE^fE2ih9nTGoq9;j2G3{cO-``&2bD(vr8*=azkALzLuB#4 z`2iEYNY#7wBKGpn;CNmaQMw2*MMNuw{%*dG1^Kj`SyQi=%--)@+)0+o7zTxxe-K+0 zesNEOuR}kBcYuH!1aVX+i!(n|St$>?S_k4F6cE23gKQk&09Q&AVJlTIE!TS}b#K$! zAB*-Vv?p7H`@5{+=EwY^gQHnI3d@`+ErkP$#yy9?#qq#u&fy=hO?1sNxCjpyj`pSu zF#S+OJaS4sbT|BAZMy|Lqe>pc7?zxAzSF8k*@Oz+n?k38IX`0MAa17blK_*Tf%@jc zZB7ruV)B*lf1OmA>+w^r(DBZKwefl>-_6jOD))y3h#*YiC{Jm_&IL-prq3qLJGM#b zhbbJt5<)2s^*a_S{MfP;B{HOH9L|0qf#J1R6Ms+Kf7v zMMv~+Wi0Lrtz@N!IYX=ze*#J4%{N=~WzON+gmPUuOmZ5DhWl^7NoR8UP;k=y#dd1J zAtwPS(ImeGv56O0#qvMewK976r!c)`yHff8c6}bk0#|YQ5>8UvrdvFU+QfCw{HRps z#m=EZXfPag#vk@#R z&FJ1}4Wji=&Lp$Q2>_a=t5B>@EbGmw&50RlY+)!msC%Qwsvo zM$%O~E78WP#jdhUrbAa-?ExIizjM7R?YIdtxSy`6TfP9G<2Z1p z;U3b%j=@ir+%FK5{+jC=xjhgC93-niO_!7`;M^>sh!? zMxG66i%x;TC~L33bd`K1&GYMNOIl{|$);g^gOXI1`E30Brdte5(Fj2nJ-Ngcp=y}( zAxwC>0l%fB$gr_T;u!IFp6Ih8!p6)E5>8Mo?wqA=o$!1dd1EcTzr9}x5J;ckpM6_Z zPM7Mcc}_zr^hT6$3@^kfYNqnp$cALD)c6B&#%uJu2Fh^-W92m(5Wr)XKkZW3RvBrK z{U{>aOI1lbAYQ0i_Y6_Kx)PveJ6vVsGS6(X2Mj6c^qZUQ|2%&d4A1qOk>K(f^P@Ux z`ukyTn%(ycc+N#Rb=Ycxv@)l|kWo#g>nK?_uZP`Ejf-BVQ|>PLT822z;%0n(cc%NT zP|P@xfvppo6zYAP#rCTsxf_m<^dtyDn3=_Md=rIJGQ*d_cM%~^DH|>WdCos(KZtsk zyyrkxwQ1M6*hnpQS8-S0%4Op7+rL`yZnL_}fvCB~NTvB|8H`+^aClP3vK0j%w!CLpjaY&4U()NYTd^hweswS(!%Py<#85x}ypHoeKbcN)H zI0WJjz3G`wwl-%1ul%la9g%rnV1L=|A#&W^EV+cLy4hE8QN0+@{rEcgrSy5${0Ff* zLP=0>IHb5q8;;E?S%=Qve_WjV0CvsNnFSG@Y5#h;I)uk9d$8=#7SF17_hvXg0|>G> zO^Vp(`}`#IjrdY4Ya4H&fn>psaH8d_Eq9>l<;+j9>=$~pZuO}Q?NTb2Dg26Y=iZ+% z3+O7{X!)=tD`gM3;sc6IFDg+T&+4M8q7(MM-1*O~B`edjAsXU-aR8aE2&`_b&N^nP z{GV3l=L=}bq;P{MhadXX1zTaOZ%NLQDrs9890+i?#^hXvyh3CzNLjUGUVWa8f_1jP zJ`#$)cPrZ8Tj-EyN1tutqn_LRj368O{@mt_P=}xyUSIXWam@G*W;CBvW`6* z>wb>*dirq8r~YUs_vXU%_D_ZfkeoHGqY1#sn|oFAbFxcl)LYk4%1W0 zkhDI%#1>3| za3qB*l(Xlf#q|{RY z?h~nGsfO@2ZY}j9CH%eT!s+?aV#H% zjU(v;wea&gwY(W2=a=_iI!{xOwjr1Zm&bxD{rB9e#;bV4P!olbjR_u;jHq!Wzvbr% zFnPbBNEnMKW|Zo~73U7RyV zG)Pl4=5x}6(azk0YMDM~dX(qH+5bwO+0sFZl+8yn305>mB=*bhW2})qUoP`>?S?CeY&q z?!EsHrmnT#xts_?0$;z+JZ2#FN_M{M37LrP>tzsrPY?aXW2VrfCqmXfPZ8njw)$hq zIL>UymU%GFR?KB8qW8u`bwvE6MRCPd+-`hM?f$#t6iA{2058hJ_e?bfh`mB4+a(R; zPBdSVIVnvqVGxRiTAmpCTRJKXO%0$5f*g4qi~KVA^ux?VR)k!CPoM`n)ac?LiMJV^ zvhBo2K@*XFM-JY3q7HZI%6G`^Nll3M#gh)mA)T3VT}LZpc{I)gd&ERh*M4e+z?3$& zlZ6DI8Ml5}^z%a`$WM^LsiJ_!C^&%7riOp#GaMdCReT?G8oS_KqYzsr=I$}SlH_~- zsySk(>j@C`XTNbc*%B3GOgYz3y>bs=c)XobLhAHw6e|#2xqQ5Uicm8)#(1 z@2N85Vq3_%hcT~)39Wk`8hu+zbnfsb+tY;I)&hoofU1Ducc{%fa_3AHz7 ztbqe1z7{i?YJ0{>;OIZrU)Bnw@l1WuoObaF$d6fZ_9$P49IhhbW!XuW8!-~777-Dv zjq_Dv+xEqHgjTH3gN^3Qhit8P1wQL~tCwb(;f1%a=@5U)n$Zj7ar!GmxPb5W{Ph-w zpG}W#8nM`Az_h;+rgmG~SEcD_>fG@2q{l$}!&E@je(TbY#@F~keUD<}3^2}Q&Bvv2 zLWggMXT41)DVwR_6c+T|`8ax?L+lRYSfgmtMG5}IWNOEGfXlZZ^+>0VMiD%eN4iV> zd*h4{@wi3&j3MU1si9duFk#0GLr{4bkm5nS$e^W3rb>G05o>RquOeSZmcH09Eh%LK z03Y|WDr@I5#3ywOH9uxda#U&-@(p9phFc}|Rf|INLf}00L4Wy4NrC5u&vF5EI3M~m z6KOvji<0urO1-(yNWBfXm>hMNG+$;yG zls_CGR=_9-povk1C-hd? zfW~#%+HB>3)J7sl>R28pws(PR=YdGT*etPql7#oz!X8SsRdB%?SALBgkM^x)>(JEo zH7y&_nN{#ahv+=sqX8r$yuTOgeE`PAe={r(WcFw;Azx0!JX<_=x}b9?lG_4W8h#H3|zERfzuUTT|LSd)D%!C zO3W5<`sOAYNem1zG|8OWY?3;6XHHhodB6S4>YHj({D#ma=W`|e?JykD^b^!#)nJWY z*D|8B%1$Y;GBGf49D03cX?d)AzJw*4>S+k)PN8&WUtSD=n4ped%+oWu{fWO6w0ST7 z$2lesTY^Yt-n$c00rKYd1hd zk9L8y(1S|)eamrNs%=26P7$=gm!Y6J0Rms9^Fg^y-bC>c`>cT1+5$k`(n*Yn4V=w$ z7aSmwLt0%agE93Y*}xek%#<0kv+(2pC0|ie6u9!G-fa556-a{VZGi{ZPps7NIMdQV zK!@`l9cl4O2t1aNpB+{T`Kb1<&*}zXjwD!&cRBR@^W`!`t4~vU8*!c*t`)YuO6|8> zeyB9TfY;A(qt!`W87J*uzU86^ru^4vaB)Y`4F43U0F_K61WE`*{-KYX>i>(ZCTFgb zIsKRH6Q+qQx$dj1SK-d*+cn+eqU>KuEDoa=mV_j+xcIZOy9@<}+ef&IE;&)X*ts<` z*NOyQ)LqtMZ4i2I@IpxRz8FROPVXw>iW9sG?lAyI4Z1B{H6beXB@47I3Zk2e1X345yZjG)wk6=A!+&>tfoe&NwCG@ z3Y1L`;wp{Rzf6ABnEv$mK;~SFg~3%_jeRl3Wy=Tx+nCn2YXIyBz*bgTI4iAj?2c%Q zgz8+7xpfuR7^(5dH&G>3XnB*cl!1fN{A&y~en;=j$?sQ{l3ZzCK0LIg`UYSkcHaeK zvo^`YDE*`HeaBf>Wn|m^wLS4L8Q1w4JO@J21|Yx-oqp9DJYK7Zng+n{ZM6qBX{1pV zPpj>55$bwqo6a1qHYJ2~{PnpR9C5ALl8+0ISSVtljhmy__4(+`I`CJUrM$yil{4=u za}&@}~kW1x`JMKo$@isFd>TpZ{%&RlP{R=2s2-ogN1dVTAWaYG(>q+}@a4lH z1H5tr2%4|o<0@_iDlH-1VEm7v0jSoEL;5+vqtVk1$Mkni#g^fPS=o<+Ksqkr90-C< zCq{XJu^oa+Cy6{$kjUXQGNs1;8d+k;|MP!!$mC67yj1I z5yUd2Rnp&eLMY>LLeW8dGhjqG3}8`U0B`-4)Vv*xGO_m?ROpRv0r+K5eH8M3dg~r+#omS^uu6L)0 zHXoK>yhl}9g@)ena`X<*g;P{cmdaG9VT2jH=3K2yVbaD}?U4TQ*voJbhl;el3xa-0 z2TNq#s(jn2W5OGkt84FEkG-&W*D);-YueQjC%S;c001KuNkl*T_IbpC7;UQ>{yl%f0b8+|xhm9eF+-eGG&ZKi(tzem-suL3?cvIv)C%&%}WrVX^qWAMp%+6cVQ#g}!d2 z;ibk)C^hVd;n@iKF%-iuHX`x;e7qIY&pa5-ryA|@~G3#)JuB#sFDX1syGjnO(-)PPA3IqYfu-j;sFE}zEW zMsLsp2HmC^s4B|_a9*I5ZyPbii^;ZrOfTbj=_bxJYq}MJu#xF!9(3acFTUm3wk`-> zk_{`jG&+5aZ2)ZQ8a4_zv=K*VL1wOE=PGyxR>sOs$cq(PA@On-c^V@q!Jpi%F2F0x%5K}WMc||Jppa&(p~p8 zBW;au0G_(%S?qv$u2G3SvVn$>tBkn{nqT4lUY$6im>%9hA9o{wcsMI5{Y+&gj?4Hd zP9F3ljfd0E`sZ{s`Wia;0jALaKx|bmb0CoZ9i`Vmd>sr90SYTk|B=45?E6NKy!LRH z^zX%phB`w_O}9duK22x!>9=_uNV;7cfFRf)Ywz4tRRJeN-1r8-C?0$_A^pp;InJRl zK4QsAIN0Sk62#+|soeN6l@W}5BaPFVFmDcT=JSxQ&<@QZ13ntpgpt3Q8gN>WqYp=C zB|V+yRGmA714IW)vma)4w&_xxd>BuEKfXb~EsbTy{|2!ii5D#0P@%;Bf(D1bejU#WdOZh+P}at(*}W`qXEinzj} ztK>9b$^*-sn#9el9uDx7M)9Xa_K+zo5E+4$DT{e z{*|L7gVBIVbAst-9!XENn3)6ZQRS9zP^wBbRX%2#1dmeT7t3Rbk|62EpI3=)EjF(SH=IfV3cT9tXVbk)i1#kAm|?nKjX z$|_F#r4E80%bMHqO{?a((xq5$eekLGdW-P+ANw`Wm}9SP4K{Ds&fz=)=%)=r zey>gAVQ;Unum@up&C}8-oxfM}CUaM#{MUXVSvafTkx7{WSZy(iNSOVHDSXF2tVA=DEU& zTqAT@e8bt2$$T238uEHDlN4eQTEX7HFSX9NoAVGIHYAbvn&JPjn~`x%_I*2OX5 zdGRg1i3VPzx9l8*4;vf$nTE&`;+C}5DXzxPX+R3!^BdTJ=}Zmp24r@l*M2E2=~K}P z;PhRpPrVH!lBa)(^rHbi^6#T1yQhMFgFMkrq|MqzGU?CaOLqNF*8OEtTL6vv4dfh| zb>6?r&UHA;s`0Mz4Zx!1m&ic&TSzJo4{4}l#Z50p1K{k8bu@A`NIC~;7}hAPp)km& zSDet`G$0a1@o`5#Ff#s1M;bWak9ho2*~-sEN@G_bj5o8nwqgtU;gn}#K%=p@fZqqq zwKtL8Dz6~@XmpPK2n&2@L@IRwM5WHM=Q1#krrF2i>DK#V>8Jkv>)NaV;F})V@ZBK8@Ps0=7@z7D=_)IIw=MgdsYsKs&gklBRc zIal$~LkAPkK*6Sb8NO`F$h^=c$M1vjSNjhmS*B_N&lpO}U4!b}MjlkHq zv20dhYq*+W^j1GcR=iX=X?&-=Rem{ne%vgKmj;=H0GEFK|_W{w}_NF#3! zrzFHiq6{7-h6goO)k#dDvF*O`NYO~Oq(6(T-N%2NOuCCI)e4@0JcSauUY=gn*9dj$ zKLY4o{(ih=!}G^ZZ$yI1%HaHY03(4pwvnYF7rV%Z{^}H0TKcP>kCzHpJ_;WC~+ zrbGRFoPOp)x;YXFp=X=BSxDc_GP8HT?CN{0-f690ZZ2AJf=pX*2WsqJArY9$Lp86=y8Ym5z6_?P* ztZN0{BY@u9dSu4z{|~!P{uK&Os+ZD<@WqO9fOF=; zsqV2Ln9`Igz1Z{nBYZ#=&8A|$O{1qWtEC?)vufDCL93NUQXU(zRo z=DJ`u%>8x!CLfrd_*HfKxmD)T?v_hu19EvBGI2RZ0yAaK8y=ADYZBcwlw1R_aOr$Z zOz(rT??I-t?zq$|F)nh+eB@^h3(Ks~bn%L<_Znu1iu}am$LY(3M=Wvtw@iVt@zG$m zMY5d@)4PsJILHY;<oTI)qao6KJQW4v37e>*^o>FFn{SE!4kd! zrA|pov4-&Bg5W-6!5P?L=Xz`djc^(htDzc|;#xIh;j!BSe(cFr2Kkp^R}fy{;R&1! zW~X3L^F_2|Oj9!*_9@6!*xWW9`e>S|kK$%TgZpaJ&&PkSBvW0RnLR_4Jq>l}W|_9= zn0)xXkII7&G8c#BhE`h^-#A11`@e+JeCLp~Xm;c`;pC9sL`POgQMno(Pjt2P@%Xss zg5dpFZ{67>L&0|#QN3(_eQS&eM{FJEav*q6y3;qx+PhyG zN|w}KQ|7ODyM&qhQE(RwMS+%x%iV2rFbd!o5Y1$~h*t6X8@JbZl)N4fk+t_P$={>V z39|Bt^X7dqeK_RC&O5;~e8JY=hB_Wwb`75Sb2>t&x|<%~OIt1J=ccDe`2GPC-El4l zPDP#ipiE!1Np`KTdCx%g4ZtNIA0rb_#XkP*XQ9aWwkQr7^EM1I&&DXAWR7U%BlPh~ zbAWNQ)hHH!FuzJ=IMq%?BQO`&el!9H8_*D(g?zgj!+vfeD(Qxh{%0`v=k%uI;yHj} zQFt91fq8QFMgJjNpQ!p1GFt26jEmp*-hglX^{+%dUQnE1nESFKUtZ$nEWiFTnQF^g z=gWo1v!)zTVQ4QV% z4)Wv2@p%mrB|j)`K|aHLN_$s=o@WT~Ne74K*a2xq1eKO`H1ggbT*gZD%I=z z>JBCS{51OGXuvwITn?OojVvFK8H=}KO7LVkVhz`#(fi_%&-b$%__|_iKie;^eRaMZ zF<*_jEVl~lyqz2r*Eww79Jh#wIDMQX9zRYW50A$$M_(!&uha_82QX8f?G;WDm?yK5 zAE%)q;q-!webwq0*FPS=l)jJQ)y1bq@Z-ODzLmc}D5?3=r0=$hrUR8ttR}xWO7;YN zW2wCO;ZA|hP5uKz&Ua$>iYz~aYV<4`WR@4BnG%IrpmVO82U$-rZl_}9cDsHiT7e)dTVE}Z@DXss!SDcWp zY>x&SjYE3HH9q}(7v~p`P&Pi8DGm85uJYpPi6@YTtvWD%oW6h`2sDn7S7m(0@f~Ha zK|TI8>vG3co&&)*Wy5N2#;eG2a95e@>iz^OgzGjci!!XKaFn>*kC6ajB~E)nLSqPs zq>wCpgCS0*(B9?$#`%%f@b=;j7C)a`;hn}ce0|>V@MRaqEbimSA#T#_U@hrq|JBzS z`WMgalD@~JEA@$zp+y2JUj*E-_Enj-@C5LqP0jFAmOPchZV_jwbK^CvT-8=1QB$Pf3b zdz2pfk!c`S)R3 zg$-_|M$-t?5EL3i&~QLjdQ;&YByJ4DL1QckQ)uXI6vim(j4Xsi@*Q53xR|FiTw|5M zRd?k_9Sly$_@r^r;Jk71{J0T6FOG?aABEMXpL-Xc>TdSffb0F;b;eCNolksyF{ z^eaX0$2GVT65{gZ8$O;N>HTottFDCc_zaIblI7_~4>|`6_#-Hf-Vo=07)Gbi=l>9< zUVKNR<0{XARQkix_vObck6+8~m52jnL@{B#IkP{3x%;U_3vcR^!v~%n2rd#OV~;yTO0s z`o#J1KJRk;Dawh9;NX&MXj~qB)sHxNa>4ZxJooRoYH}cyF4$wh9a`19+QwPC*KNTG zro9lyYh*b-H{XQm0Jp!6$5pG-&QBrv_;^Kpy$F7f&A2pP({pRRAT4n(4(TGN{)9dr z5hRPmMNqW|u-3Atwknyda>IyQD!5D5uliiA8dpC8P)T7Lj~fEBG^So72J)cHAEL7I zF_rd`OgwRQikAx4-jC47OT||{3eAvO4I26wc@FxxG3H|75M=0_K*b4bxyVL&__*@& z{fPT%CcYNCj$ZDp`ZH@Pc0q6mYf}HUV)`P4wKuoB*X@_-3%OV9vLbxj-;1{QG8%xr z$ikDcJvM4eVQi7}@yOF53?malAE#e2r@S1!xy6rq^ARI)^Qh4vxKUV)U))b=33<^e zB6$C`pwgTEhlCcy2 zCo02Bg#jbTXA3Itcd+)xjY7XdJ^+WfS_i z3Cz((JY9TEj`3GNcp86&j<0{R8O)_Kjrz#~bMOXU> zfE;;1;f#elP|g)^JuIcju=eMMq36*MaQpBSw{^JM1jTJqZHKTfP8loo4Yr8DxPBy2 zzar!ndEt8m@g9TG*FePeDW`8aeuvQa(D+{)+90&v z+0v5y19?E6zURs$bW0}p)56DsuBIfR)-dM5Cj{Tdl;BzH7~(?$HUg%Z#g9TFCbt~a zoKwi;)4&v(;WzH{prav~xfrf~#N(HSm7s@rB$K!{JcP#hou83G0yyI86xVR3l{g_U z{wUms#l9aPk2=QBjH%XTB$fJDwUJ8E4{ZvdTz9VDhN-|@+@7yqBN}E<(_cbEz+=Yn z-2pQiV1}rX5HILa)sKO2zR^WiL<@2k4P1Fd^v;hol%+gOIDU*DZ|KNJJxK535*p|H z3>`@wt%j>?!aQCsz9TmxlqKO#s_635NIz7w7~iCO<*iS>^4sx>GOK<0`584+Itu9BM)4ag!6+X zk7IxT6S#)`YCrbA1cFm|gULue3buhR2>z=)dG~`!N=#-AKs8F8{TXD=^LkrX7|0$J z!rw|)&q?z1>eF$8%|hHiR9BfpVa{!?zk&gL3*RH#fW{z$tT)C(E8<0`5U38d3C-pF z7)RqNjN*CmL6+|{j1ZT`RepIgOkhrjd`9lh(HKU+&8>re5Lf!|=bA_x3=D(dKD6HF zU;*wtnV6o5jQDfp<+h^PleA|LY>*k}d?9hRNi&q*W!GSr*S{yuH)6=%UOp{d-CvTm zcjDYG&t3k3(X#W%w^142;`z2F$}bbo$I}9yM9i=0&;YeprpB~W8j{*=+@pY=M(ubo zzT$+%oXRRS?TE*Z)0Yd+K<-BD>tBbfEr$HTx9~drQdx653e0mnFEf1RdaU^V8Om#c z7m_TRMx9=N&ySL2>Q%2q8P8t^MH8P2{2HsD|0D~py;r{WzS0ldhf^cKOy>ccXW(e< z({cIl7L+r%c!5<*ACF&fA8&!!kfobTnXd%)5cFuFIx5v zJo)(qvf{V=pdsUdd>5mRYlh6H2m>v>B0)p&>b-seju3we8IiyoDfkiU+CO5L{~r}= zG~5~iO&Avn&%jw&r{LnKpc?1dpNTezeO446mmTL952t*-!_OXnD;|NijjO=;<7w?( zaS^_n_2*dcxE2lL&|1L&hkJ(Ug|cDgb8#_A+}Ec8pk@n}{xceYkJO|j7xqN%k)Rt# z9NqO)O+1EcBf!Mc_~)V#I33qCW(8c_0OJO4sVe_XlHYE$^Xp;w+px;tR;~u)_tW2d z!x`uZ{}j`)_rn_tKsC5jipP9mt)V`yeht8)6(`6*7Ohq|wPMu+c@2%m7bP|7)6#dx zixtN!ML3)r0ZkL92(vNmoP}#P8iP?KW_K1+Ui_W_?>l6d_Z0BYI0{6mt>e!i^2 zfxLn)?sL*J`U~>pSND~ruZEx<8Ua7OJg1)x!7PprfOm7dAzc}RwM5=;Ir?}wq8`I& z_7xcWix}c>2jqsQwsZ*^zn?!#ZXGK}4&EYR_T%uL=LZ$VgWwiSeG}XCQ%pvEOYpjT z)3W{>PeAaZV#49~j9J*5Gwn{f?{ioMtCMTDM!?Uev1kC;DCouS81Ukkf_0iedc_Ip z;vXCE}s7Ob&}2CObsdVb?sn{EIsEU;R#%I zboFfjsQAKVmrF+e12xcF3<(~QApA#cY5WcoCWDDeik-sSJY-hf*%O5KfJaQ|1$$?xBaJ9Xw=N#-U*_88aAhvM%V%W&-t84H%ZfK}PkDkbmGjgTDR=e4<`bc8u?SUv($zWBx&F#fqCAf=roR+|H? z7w0cKvD#?Eoql9B0GWP1iWvzjS;M_vHbeik91lm_arYx<3b6RbaWZiHMa;3LG^2KP zWDbiQ*e_GYogxo^`QWh7HUT9^&In+jAJ1Pq#-Bi)X=fIbWbffWXy*jR(>5|106upO zg_|!ru9E}ha^Rz~zi~zEDSG(gnIZ$a1WHTeCzO;V>uwdD5qufhux2&{Abm&G{4lKgktZy3$X>R`p zAmp)49oNZ$DsvzVMnt`7@5g|>H=HZk{wJ#}`VM&~2W$?cgA1`G_gC$l9ql~=2>bcA zRmXL5ptc;yhHXCzXx|Y)?@ejYG**IGXo2I45y7aC}u`q`6?bq=IiF z4}Ok3IBOW>!4%oGVZH3?d%B%Us?83^42Q$UpH0%;{We*9*E6OfU4C1a?0WW1cw6zE zXuRG9cxF3Qu)MSc0gi|dzALHlyRvT8n&?F6^OpTDIffH?C1gX;%8vExYptwcwYs&> zBH+ShZ;(v*aJ3oGV0vsUNxcb`|Iw0i7vFHUWcsgyp}Y~N#J&-9CzYga{eWj+_QtsLKR;2Ipnt9YaVknd9EuxY9 zTevr=HXBm^2Qf0$#%d7#K@9zGkgvUOX!@(?-jyBef(m12UshGhJd#?N4-Hb_%IA;p9WyV${)bUc=*SLZE0P!a@o@v!9*8LUr4 zrS+?^g**7eM0h31BnW;fLHbfmYkr(0+h+4>p9X+Axo*`%vhY3UWB!Rpfx)ZI&Riou zz^ZZIs((vPm*AD;cqPdxo19)7R^J27?nqL*+Ia<7xD0MW8D1iNcRyY`pVo}oUJU^A zn8WP8RbRkp;7kl9KZgt7(r%Iqf|n$nPWG7TuvHs?g!P&-?!zcGu6;Di9m5lhzsDon zvt+~SJEKRI&C;e$a@(%~(2QEQa<4pn&wr7i>s++p%^0?Y5~Oaz(0{M0lnCrjRkyO9 zh|mg{bcf06#gB8OgBu`uXzL~ws9s1Cd{er6=3~M3ukf+ZLxth(EQNn05r$?R)0r=`Mubb@)z)`@28*4j<>V_n~F{puDN|bMl^ih-b(I> z{POMtbx~->*?$YZzo-lEh^bjG`jA;asz;+8$!R^LeKrDBlefJem?hznzsFmHA3(IE z^WX>`Wi7!Yu?^LzR2A)lWt*{MZf;d^>Px&yHr#Vs-9#*0db(ug$;8iiFzZ9X7bKm! zE%B*Z-7+It#v^A0;OAT4S6{*Uz|ELrUIhDp#1BdU|I@k}zf~~#;r7rHp6vWTt_wF8 z1p&Y8oS+MSWY&kCUj5OLYW&R1k=X!fHR6%a8&+S6^?~bA!Cq4D+F|fL^n~1yRBzW> z4L~6E*QamwS5Zb^A9BWW9VnH$2Ic-1Jl1QgH~VVIn~~iBXpZ%*UMXWHo{iA}=9@Ln zuY+I*9vR)DEb9#G+v8TPe8P_aOUX~CZbi_`rGm@&bKU39%{oxus_&Me9WlY320)8? z&FzQLEBp@(iq65RK=rSDhN+vea=cU7)fYC%;3`ilI4JpY!e-%K2oar*>z8h}z|xnbqc z+i88PZnT;>zz;M1& diff --git a/Morphic.Client/Icon.png b/Morphic.Client/Icon.png deleted file mode 100644 index 497994e4fa4fad297971596a7ccf3b6a85155ad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6613 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 TxVxm(00000NkvXXu0mjfOTxzP diff --git a/Morphic.Client/Menu/MorphicHybridTrayIcon.cs b/Morphic.Client/Menu/MorphicHybridTrayIcon.cs deleted file mode 100644 index 7fff0482..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 != null) - { - _notifyIcon.Icon = _icon; - } - if (_trayButton != null) - { - _trayButton.Icon = _icon; - } - } - } - - /// Tooltip for the tray icon. - public string? Text - { - get - { - return _text; - } - set - { - _text = value; - if (_notifyIcon != null) - { - _notifyIcon.Text = _text; - } - if (_trayButton != null) - { - _trayButton.Text = _text; - } - } - } - - /// Show or hide the tray icon. - public bool Visible - { - get - { - return _visible; - } - set - { - _visible = value; - - if (_notifyIcon != null) - { - _notifyIcon.Visible = _visible; - } - if (_trayButton != null) - { - _trayButton.Visible = _visible; - } - } - } - - // - - private void InitializeTrayIcon() - { - if (_notifyIcon != 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 != 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 == null) - { - this.InitializeTrayIcon(); - } - break; - } - - // create tray button if requested - switch (value) - { - case TrayIconLocationOption.NextToNotificationTray: - case TrayIconLocationOption.NotificationTrayAndNextToNotificationTray: - if (_trayButton == null) - { - this.InitializeTrayButton(); - } - break; - } - - // destroy notify icon if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NextToNotificationTray: - if (_notifyIcon != null) - { - _notifyIcon.Dispose(); - _notifyIcon = null; - } - break; - } - - // destroy tray button if no longer wanted - switch (value) - { - case TrayIconLocationOption.None: - case TrayIconLocationOption.NotificationTray: - if (_trayButton != 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 d80f7f46..00000000 --- a/Morphic.Client/Menu/MorphicMenu.xaml +++ /dev/null @@ -1,65 +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 fdfa0deb..00000000 --- a/Morphic.Client/Menu/MorphicMenu.xaml.cs +++ /dev/null @@ -1,278 +0,0 @@ -namespace Morphic.Client.Menu -{ - using Bar.UI; - using CountlySDK; - using Morphic.Client.Config; - using Morphic.Client.Dialogs; - using System; - 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.CopySettingsBetweenComputersMenuItem.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 != 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 != 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 == null) - { - this.Placement = PlacementMode.Mouse; - this.PlacementTarget = null; - } - else - { - this.Placement = PlacementMode.Top; - this.PlacementTarget = control; - } - - this.IsOpen = true; - - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("showMenu", 1, segmentation); - } - - private CountlySDK.Segmentation CreateMenuOpenedSourceSegmentation(MenuOpenedSource? menuOpenedSource) - { - var segmentation = new CountlySDK.Segmentation(); - if (_menuOpenedSource != 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 Countly.RecordEvent("morphicBarShow", 1, segmentation); - } - - private async void HideBarClick(object sender, RoutedEventArgs e) - { - this.App.BarManager.HideBar(); - // - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("morphicBarHide", 1, segmentation); - } - - private async void QuitClick(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("quit", 1, segmentation); - - this.App.BarManager.CloseBar(); - this.App.Shutdown(); - } - - private async void AutorunAfterLoginClicked(object sender, RoutedEventArgs e) - { - switch (AutorunAfterLoginItem.IsChecked) - { - case true: - await Countly.RecordEvent("autorunAfterLoginEnabled"); - break; - case false: - await Countly.RecordEvent("autorunAfterLoginDisabled"); - break; - } - } - - private async void ShowMorphicBarAfterLoginClicked(object sender, RoutedEventArgs e) - { - switch (ShowMorphicBarAfterLoginItem.IsChecked) - { - case true: - await Countly.RecordEvent("showMorphicBarAfterLoginEnabled"); - break; - case false: - await Countly.RecordEvent("showMorphicBarAfterLoginDisabled"); - break; - } - } - - 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 Countly.RecordEvent("stopKeyRepeatOn"); - } - else - { - await Countly.RecordEvent("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.ShowAsync(null, MenuOpenedSource.trayIcon); - } - - private async void OnTrayIconClicked(object? sender, EventArgs e) - { - if (this.App.BarManager.BarVisible) - { - this.App.BarManager.HideBar(); - // - var segmentation = new CountlySDK.Segmentation(); - segmentation.Add("eventSource", "trayIconClick"); - await Countly.RecordEvent("morphicBarHide", 1, segmentation); - } - else - { - this.App.BarManager.ShowBar(); - // - var segmentation = new CountlySDK.Segmentation(); - segmentation.Add("eventSource", "trayIconClick"); - await Countly.RecordEvent("morphicBarShow", 1, segmentation); - } - } - - #endregion - - private async void Logout(object sender, RoutedEventArgs e) - { - AppOptions.Current.LastCommunity = null; - await App.Current.MorphicSession.SignOut(); - } - - private void Login(object sender, RoutedEventArgs e) - { - App.Current.Dialogs.OpenDialog(); - } - - private async void ExploreMorphicClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("exploreMorphic", 1, segmentation); - } - - private async void QuickDemoMoviesClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - segmentation.Add("category", "main"); - await Countly.RecordEvent("quickDemoVideo", 1, segmentation); - } - - private async void OtherHelpfulThingsClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("otherHelpfulThings", 1, segmentation); - } - - private async void AboutMorphicClicked(object sender, RoutedEventArgs e) - { - var segmentation = CreateMenuOpenedSourceSegmentation(_menuOpenedSource); - await Countly.RecordEvent("aboutMorphic", 1, segmentation); - } - - private void SelectBasicMorphicBarClick(object sender, RoutedEventArgs e) - { - AppOptions.Current.LastCommunity = null; - App.Current.BarManager.LoadBasicMorphicBar(); - } - } - - -} - diff --git a/Morphic.Client/Menu/MorphicMenuItem.cs b/Morphic.Client/Menu/MorphicMenuItem.cs deleted file mode 100644 index 56da8a8e..00000000 --- a/Morphic.Client/Menu/MorphicMenuItem.cs +++ /dev/null @@ -1,202 +0,0 @@ -namespace Morphic.Client.Menu -{ - using Config; - using CountlySDK; - using System; - using System.Diagnostics; - 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 enum MorphicMenuItemTelemetryType - { - Settings, - LearnMore, - QuickDemoVideo - } - public MorphicMenuItemTelemetryType? TelemetryType; - public string? TelemetryCategory; - - public MorphicMenuItem() - { - this.Click += this.OnClick; - } - - private async void OnClick(object sender, RoutedEventArgs e) - { - if (sender is MorphicMenuItem item) - { - if (!string.IsNullOrEmpty(item.Open)) - { - Process.Start(new ProcessStartInfo(this.Open) - { - UseShellExecute = true - }); - } - - if (this.Dialog != null) - { - App.Current.Dialogs.OpenDialog(this.Dialog!); - } - - } - - string? eventSource = null; - switch (this.ParentMenuType) - { - case MenuType.mainMenu: - eventSource = "iconMenu"; - break; - case MenuType.contextMenu: - eventSource = "contextMenu"; - break; - } - - - switch (((MorphicMenuItem)sender).TelemetryType) - { - case MorphicMenuItemTelemetryType.Settings: - { - var segmentation = new Segmentation(); - var settingCategoryName = ((MorphicMenuItem)sender).TelemetryCategory; - if (settingCategoryName != null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await Countly.RecordEvent("systemSettings", 1, segmentation); - //await Countly.RecordEvent("systemSettings" + settingCategoryName); - } - break; - case MorphicMenuItemTelemetryType.LearnMore: - { - var segmentation = new Segmentation(); - var settingCategoryName = ((MorphicMenuItem)sender).TelemetryCategory; - if (settingCategoryName != null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await Countly.RecordEvent("learnMore", 1, segmentation); - } - break; - case MorphicMenuItemTelemetryType.QuickDemoVideo: - { - var segmentation = new Segmentation(); - var settingCategoryName = ((MorphicMenuItem)sender).TelemetryCategory; - if (settingCategoryName != null) - { - segmentation.Add("category", settingCategoryName); - } - // - segmentation.Add("eventSource", eventSource); - // - await Countly.RecordEvent("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 (((MorphicMenuItem)sender).Open) - { - case "ms-settings:colors": - settingCategoryName = "darkMode"; - break; - case "ms-settings:display": - settingCategoryName = "textSize"; - break; - case "ms-settings:easeofaccess-display": - settingCategoryName = "allAccessibility"; - break; - case "ms-settings:easeofaccess-colorfilter": - settingCategoryName = "colorFilter"; - break; - case "ms-settings:easeofaccess-cursorandpointersize": - 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 != null) - { - var segmentation = new Segmentation(); - segmentation.Add("category", settingCategoryName); - segmentation.Add("eventSource", eventSource); - // - await Countly.RecordEvent("systemSettings", 1, segmentation); - //await Countly.RecordEvent("systemSettings" + settingCategoryName); - } - } - break; - } - } - } - - /// - /// 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 dc7be448..00000000 --- a/Morphic.Client/Menu/MorphicTrayButton.cs +++ /dev/null @@ -1,1380 +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; - -// 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 == null) - { - CreateNativeWindow(); - } - } - else if (_visible == false) - { - if (_nativeWindow != 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 != 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; - - 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; - - // 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(); - //} - } - - 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 != 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 - - 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 == 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 == 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 != null) - { - if (calculateResult.Value.changeToRect != 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 != 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 == 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 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 != null) - { - var taskbarTripletHandles = this.GetTaskbarTripletHandles(); - var taskbarTripletRects = this.GetTaskbarTripletRects(taskbarTripletHandles.TaskbarHandle, taskbarTripletHandles.TaskButtonContainerHandle, taskbarTripletHandles.NotifyTrayHandle); - if (taskbarTripletRects == 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 != 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 != 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 != 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 == 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 != 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 != 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 == 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 != 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 e2824bea..00000000 --- a/Morphic.Client/Morphic.Client.csproj +++ /dev/null @@ -1,162 +0,0 @@ - - - - WinExe - netcoreapp3.1 - enable - true - true - Icon.ico - - Morphic - true - AnyCPU;x64 - 1.1$(VersionBuildComponents) - localdev - $(VersionSuffix) - Morphic.Client.AppMain - Raising the Floor - Development - app.manifest - 9.0 - - - - - - - - - - - - - - - - - PreserveNewest - - - - PreserveNewest - - - - - - Always - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - Always - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - - - - 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 8912b9a0..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 - - - Customize buttons and take your settings with you anywhere - - - Advanced Features - - - Make it harder to distinguish items - - - Turn Off High Contrast - - - Off - - - Makes screen easier to read for some (Right-click to adjust contrast) - - - Turn Contrast on and off - - - On - - - Contrast on, off - - - High Contrast - - - Apply color suitable for the daytime. - - - Turn Off Night Mode - - - Off - - - Reduces eye strain and makes it easier to fall asleep at night - - - Turn 'Night Light' feature on and off - - - On - - - Night mode - - - Drag with your mouse to copy a part of screen. Paste into any document. - - - Copies a selected part of the screen - - - Copy - - - Snip - - - Snip a copy of part of screen - - - Contrast & color - - - Contrast - - - Color - - - Color Vision - - - Turn color vision filters on and off - - - Changes colors to adapt for color blindness (Right-click to choose type) - - - Dark - - - Turn Dark mode on and off - - - Toggles dark mode on and off (Right-click to adjust) - - - Dark mode - - - - Night - - - Night Light - - - - (Right-click to change magnifier settings) - - - Turn off Magnifier - - - Hide - - - Turns on a Magnifying Lens (Right-click to change settings) - - - Turn on Magnifier that Follows Mouse - - - Show - - - Magnifier - - - Show Magnifier - - - Hide Magnifier - - - Select any text and then click to have it read - - - Read the text that is selected - - - (Right-click to adjust speed and voice) - - - Stop Reading - - - Stop Reading Select Text - - - Read Selected Text - - - Read Selected - - - Makes everything on screen larger - - - Increase Text Size Everywhere - - - Increase Text Size - - - The text and icons are as large as they can be - - - Cannot Increase Screen Zoom - - - Makes everything on screen smaller - - - Decrease Text Size Everywhere - - - Decrease Text Size - - - The text and icons are as small as they can be - - - Cannot Decrease Screen Zoom - - - Text Size - - - Make all the sounds quieter - - - Turn the Volume Down - - - The volume is all the way down - - - Cannot Turn the Volume Down - - - Turn off all sounds from the computer - - - Mute All Sounds - - - Unmute by pressing the volume up or down button - - - Sounds Are Muted - - - Mute - - - Volume - - - Return to the previous volume level - - - Unmute All Sounds - - - Make all the sounds louder - - - Turn the Volume Up - - - The volume is all the way up - - - Cannot Turn the Volume Up - - \ 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/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 c039f252..00000000 --- a/Morphic.Client/WinApi.cs +++ /dev/null @@ -1,1000 +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); - } - - /// - /// 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 a7e8f432..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 == null) - { - return null; - } - return UserSettings.Default.UsernamesById[UserId]; - } - - public void SetUsernameForId(string username, string userId) - { - if (UserSettings.Default.UsernamesById == null) - { - UserSettings.Default.UsernamesById = new StringDictionarySetting(); - } - UserSettings.Default.UsernamesById[userId] = username; - UserSettings.Default.Save(); - } - } -} diff --git a/Morphic.Client/app.manifest b/Morphic.Client/app.manifest deleted file mode 100644 index af7f700b..00000000 --- a/Morphic.Client/app.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 973f90b0..00000000 --- a/Morphic.Client/appsettings.Debug.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "MorphicService": { - "ApiEndpointUrlAsString": "http://localhost:5002/", - "FrontEndUrlAsString": "http://localhost:8080/" - }, - "Update": { - "AppCastUrl": "" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.dev", - "AppKey": "b518ed6ca13aae3f60197dc5fdda2a0e649a54b3" - } -} \ 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 14f3bf3c..00000000 --- a/Morphic.Client/appsettings.Development.json +++ /dev/null @@ -1,27 +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/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.dev/autoupdate/morphic-windows.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.dev", - "AppKey": "b518ed6ca13aae3f60197dc5fdda2a0e649a54b3" - } -} \ 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 e9928b17..00000000 --- a/Morphic.Client/appsettings.Production.json +++ /dev/null @@ -1,28 +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://communitynew.morphic.dev/" - }, - "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows.appcast.xml" - }, - "Countly": { - "ServerUrl": "https://countly.morphic.org", - "AppKey": "809a28e3be9fbc1e7e178cf99186af799fa87048" - } -} \ 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 cf46da7a..00000000 --- a/Morphic.Client/appsettings.Staging.json +++ /dev/null @@ -1,27 +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" - } -} \ 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 f745e177..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": "Take Screenshot", - "identifier": "screenshot", - "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.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 d1d510f8..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.Save(mock); - 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/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/IMorphicResult.cs b/Morphic.Core/IMorphicResult.cs index b6f7ded8..4eea505a 100644 --- a/Morphic.Core/IMorphicResult.cs +++ b/Morphic.Core/IMorphicResult.cs @@ -4,7 +4,7 @@ // compliance with this License. // // You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt +// 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 @@ -153,5 +153,4 @@ public MorphicError() { } } - } \ No newline at end of file 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 index c2c1f1b3..a50a42dd 100644 --- a/Morphic.Core/Morphic.Core.csproj +++ b/Morphic.Core/Morphic.Core.csproj @@ -1,14 +1,8 @@ - + - netcoreapp3.1 - enable - AnyCPU;x64 - 9.0 + net6.0 + enable - - - - diff --git a/Morphic.Core/MorphicUnit.cs b/Morphic.Core/MorphicUnit.cs index a6dfaac9..9aaae36b 100644 --- a/Morphic.Core/MorphicUnit.cs +++ b/Morphic.Core/MorphicUnit.cs @@ -4,7 +4,7 @@ // compliance with this License. // // You may obtain a copy of the License at -// https://github.com/GPII/universal/blob/master/LICENSE.txt +// 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 diff --git a/Morphic.Core/Preferences.cs b/Morphic.Core/Preferences.cs deleted file mode 100644 index 969a73c4..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 == 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 != 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 != 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 != 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 93554c6e..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 Save(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 true; - } - catch (Exception e) - { - logger.LogError(e, "Failed to save {0}/{1}", type.Name, record.Id); - return false; - } - } - - /// - /// 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 77ec586b..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 == 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.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 5c66f770cdb8aa6cf7c66e0b3409d6e04ca4b439..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 401 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

3j5s@zl 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 diff --git a/Morphic.ManualTester/Icon.png b/Morphic.ManualTester/Icon.png deleted file mode 100644 index 497994e4fa4fad297971596a7ccf3b6a85155ad2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6613 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 TxVxm(00000NkvXXu0mjfOTxzP diff --git a/Morphic.ManualTester/MainWindow.xaml b/Morphic.ManualTester/MainWindow.xaml deleted file mode 100644 index 736e5888..00000000 --- a/Morphic.ManualTester/MainWindow.xaml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - Manual Settings Tester - - - -