From 4d53805ffd41b0cbfca8643c4c0a99efb9518519 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Fri, 6 May 2022 16:54:56 -0400 Subject: [PATCH 01/29] Switch back to canary track --- Morphic.Client/appsettings.Development.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Morphic.Client/appsettings.Development.json b/Morphic.Client/appsettings.Development.json index 1f120810..fe5dd0c3 100644 --- a/Morphic.Client/appsettings.Development.json +++ b/Morphic.Client/appsettings.Development.json @@ -19,7 +19,7 @@ "BarEditorWebAppUrlAsString": "https://custom.morphic.org/" }, "Update": { - "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows.appcast.xml" + "AppCastUrl": "https://app.morphic.org/autoupdate/morphic-windows-canary.appcast.xml" }, "Countly": { "ServerUrl": "https://countly.morphic.org", From b10510e3adda733e1b87fe422ffcd862789dd05b Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Mon, 16 May 2022 11:25:09 -0400 Subject: [PATCH 02/29] Correct session_id tag; move to v1.6 --- Morphic.Client/Morphic.Client.csproj | 2 +- Morphic.Client/app.Debug.manifest | 2 +- Morphic.Client/app.Development.manifest | 2 +- Morphic.Client/app.Production.manifest | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Morphic.Client/Morphic.Client.csproj b/Morphic.Client/Morphic.Client.csproj index 7ea60028..daac801d 100644 --- a/Morphic.Client/Morphic.Client.csproj +++ b/Morphic.Client/Morphic.Client.csproj @@ -11,7 +11,7 @@ Morphic false AnyCPU;x64 - 1.5$(VersionBuildComponents) + 1.6$(VersionBuildComponents) localdev $(VersionSuffix) Morphic.Client.AppMain diff --git a/Morphic.Client/app.Debug.manifest b/Morphic.Client/app.Debug.manifest index 86c117cc..548a90fa 100644 --- a/Morphic.Client/app.Debug.manifest +++ b/Morphic.Client/app.Debug.manifest @@ -1,6 +1,6 @@  - + diff --git a/Morphic.Client/app.Development.manifest b/Morphic.Client/app.Development.manifest index 59e948e3..64959558 100644 --- a/Morphic.Client/app.Development.manifest +++ b/Morphic.Client/app.Development.manifest @@ -1,6 +1,6 @@  - + diff --git a/Morphic.Client/app.Production.manifest b/Morphic.Client/app.Production.manifest index 59e948e3..64959558 100644 --- a/Morphic.Client/app.Production.manifest +++ b/Morphic.Client/app.Production.manifest @@ -1,6 +1,6 @@  - + From 79b3d11ac3ef38983c143940cd662dbf8a358d11 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Mon, 16 May 2022 11:26:53 -0400 Subject: [PATCH 03/29] Correct session_id tag; move to v1.6 --- Morphic.Client/App.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Morphic.Client/App.xaml.cs b/Morphic.Client/App.xaml.cs index 38ff7a95..0a553ed0 100644 --- a/Morphic.Client/App.xaml.cs +++ b/Morphic.Client/App.xaml.cs @@ -871,7 +871,7 @@ private void ConfigureTelemetry() internal record SessionTelemetryEventData { - [JsonPropertyName("sessionId")] + [JsonPropertyName("session_id")] public Guid? SessionId { get; set; } // [JsonPropertyName("state")] From 7db8c82c8f672794dcf68418c1b437df1b5e200f Mon Sep 17 00:00:00 2001 From: David Stetz Date: Wed, 10 Aug 2022 14:57:59 -0700 Subject: [PATCH 04/29] Support for adding a button to open or eject usb drives. --- Morphic.Client/Bar/Data/Actions/Functions.cs | 28 ++++++++++++++++++++ Morphic.Client/DefaultConfig/presets.json5 | 24 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/Morphic.Client/Bar/Data/Actions/Functions.cs b/Morphic.Client/Bar/Data/Actions/Functions.cs index b973b5fc..0c78d3af 100644 --- a/Morphic.Client/Bar/Data/Actions/Functions.cs +++ b/Morphic.Client/Bar/Data/Actions/Functions.cs @@ -581,6 +581,34 @@ public static async Task> SignOutAsync(F return success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); } + [InternalFunction("allUsbAction")] + public static async Task> AllUsbActionAsync(FunctionArgs args) + { + var value = args["value"]; + + switch (value) + { + case "openallusb": + var openAllUsbDrivesResult = await Functions.OpenAllUsbDrivesAsync(args); + if (openAllUsbDrivesResult.IsError == true) + { + Debug.Assert(false, "Could not open mounted drives"); + App.Current.Logger.LogError("Could not open mounted drives"); + } + break; + case "ejectallusb": + var ejectAllUsbDrivesResult = await Functions.EjectAllUsbDrivesAsync(args); + if (ejectAllUsbDrivesResult.IsError == true) + { + Debug.Assert(false, "Could not eject mounted drives"); + App.Current.Logger.LogError("Could not eject mounted drives"); + } + break; + } + + return MorphicResult.OkResult(); + } + [InternalFunction("openAllUsbDrives")] public static async Task> OpenAllUsbDrivesAsync(FunctionArgs args) { diff --git a/Morphic.Client/DefaultConfig/presets.json5 b/Morphic.Client/DefaultConfig/presets.json5 index 7019718c..a706ac08 100644 --- a/Morphic.Client/DefaultConfig/presets.json5 +++ b/Morphic.Client/DefaultConfig/presets.json5 @@ -317,6 +317,30 @@ } } } + }, + "usbopeneject": { + kind: "internal", + widget: "multi", + configuration: { + function: "allUsbAction", + args: { + value: "{button}" + }, + label: "{{QuickStrip_UsbOpenEject_Title}}", + telemetryCategory: "morphicBarExtraItem", + buttons: { + open: { + label: "{{QuickStrip_UsbOpenEject_Open_Title}}", + tooltip: "{{QuickStrip_UsbOpenEject_Open_HelpTitle}}", + value: "openallusb" + }, + eject: { + label: "{{QuickStrip_UsbOpenEject_Eject_Title}}", + tooltip: "{{QuickStrip_UsbOpenEject_Eject_HelpTitle}}", + value: "ejectallusb" + } + } + } } }, defaults: { From 401635566d17f41fbc8b7fe8a3a6985669b4d279 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:22:06 -0700 Subject: [PATCH 05/29] Revise 'read aloud' feature for Windows 11 22H2 compatibility; QA. --- Morphic.Client/Bar/Data/Actions/Functions.cs | 758 +++++++++++++----- Morphic.Client/Morphic.Client.csproj | 4 +- Morphic.WindowsNative/Clipboard/Clipboard.cs | 263 ++++++ .../Morphic.WindowsNative.csproj | 12 + .../Speech/SelectionReader.cs | 4 +- .../UIAutomation/UIAutomationClient.cs | 321 ++++++++ 6 files changed, 1146 insertions(+), 216 deletions(-) create mode 100644 Morphic.WindowsNative/Clipboard/Clipboard.cs create mode 100644 Morphic.WindowsNative/UIAutomation/UIAutomationClient.cs diff --git a/Morphic.Client/Bar/Data/Actions/Functions.cs b/Morphic.Client/Bar/Data/Actions/Functions.cs index b973b5fc..6162aeb2 100644 --- a/Morphic.Client/Bar/Data/Actions/Functions.cs +++ b/Morphic.Client/Bar/Data/Actions/Functions.cs @@ -2,21 +2,17 @@ namespace Morphic.Client.Bar.Data.Actions { using Microsoft.Extensions.Logging; using Morphic.Core; - using Morphic.WindowsNative.Input; using Morphic.WindowsNative.Speech; using Settings.SettingsHandlers; using Settings.SolutionsRegistry; using System; using System.Collections.Generic; using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; - using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; - using System.Windows.Automation; using System.Windows.Automation.Text; using UI; @@ -219,31 +215,31 @@ public static async Task> SetVolumeAsync return MorphicResult.OkResult(); } - private static async Task> ClearClipboardAsync(uint numberOfRetries, TimeSpan interval) - { - // NOTE from Microsoft documentation (something to think about when working on this in the future...and perhaps something we need to handle): - /* "The Clipboard class can only be used in threads set to single thread apartment (STA) mode. - * To use this class, ensure that your Main method is marked with the STAThreadAttribute attribute." - * https://docs.microsoft.com/es-es/dotnet/api/system.windows.forms.clipboard.clear?view=net-5.0 - */ - for (var i = 0; i < numberOfRetries; i++) - { - try - { - // NOTE: some developers have reported unhandled exceptions with this function call, even when inside a try...catch block. If we experience that, we may need to look at our threading model, UWP alternatives, and Win32 API alternatives. - Clipboard.Clear(); - return MorphicResult.OkResult(); - } - catch - { - // failed to copy to clipboard; wait an interval and then try again - await Task.Delay(interval); - } - } - - App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); - return MorphicResult.ErrorResult(); - } + // private static async Task> ClearClipboardAsync(uint numberOfRetries, TimeSpan interval) + // { + // // NOTE from Microsoft documentation (something to think about when working on this in the future...and perhaps something we need to handle): + // /* "The Clipboard class can only be used in threads set to single thread apartment (STA) mode. + // * To use this class, ensure that your Main method is marked with the STAThreadAttribute attribute." + // * https://docs.microsoft.com/es-es/dotnet/api/system.windows.forms.clipboard.clear?view=net-5.0 + // */ + // for (var i = 0; i < numberOfRetries; i++) + // { + // try + // { + //// NOTE: some developers have reported unhandled exceptions with this function call, even when inside a try...catch block. If we experience that, we may need to look at our threading model, UWP alternatives, and Win32 API alternatives. + // Clipboard.Clear(); + // return MorphicResult.OkResult(); + // } + // catch + // { + // // failed to copy to clipboard; wait an interval and then try again + // await Task.Delay(interval); + // } + // } + + // App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); + // return MorphicResult.ErrorResult(); + // } /// /// Reads the selected text. @@ -272,7 +268,9 @@ public static async Task> ReadAloudAsync try { - App.Current.Logger.LogDebug("ReadAloud: Getting selected text."); + /* Capture strategy #1 (preferred strategy): capture text via UI automation */ + + App.Current.Logger.LogDebug("ReadAloud: Capturing selected text via UI automation."); // activate the target window (i.e. topmost/last-active window, rather than the MorphicBar); we will then capture the current selection in that window // NOTE: ideally we would activate the last window as part of our atomic operation, but we really have no control over whether or not another application @@ -283,234 +281,108 @@ public static async Task> ReadAloudAsync // NOTE: this does not work with some apps (such as Internet Explorer...but also others) bool captureTextViaAutomationSucceeded = false; // - TextPatternRange[]? textRangeCollection = null; - // // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block await s_captureTextSemaphore.WaitAsync(); - // try { - var focusedElement = AutomationElement.FocusedElement; - if (focusedElement is not null) + var getSelectedTextResult = Morphic.WindowsNative.UIAutomation.UIAutomationClient.GetSelectedText(); + if (getSelectedTextResult.IsSuccess == true) { - object? pattern = null; - if (focusedElement.TryGetCurrentPattern(TextPattern.Pattern, out pattern)) - { - if ((pattern is not null) && (pattern is TextPattern textPattern)) - { - // App.Current.Logger.LogDebug("ReadAloud: Capturing select text range(s)."); + selectedText = getSelectedTextResult.Value; - // get the collection of text ranges in the selection; note that this can be a disjoint collection if multiple disjoint items were selected - textRangeCollection = textPattern.GetSelection(); - } + if (selectedText is null) + { + // NOTE: we don't mark captureTextViaAutomationSucceeded as true here because programs like Internet Explorer require using the backup option (i.e. ctrl+c) + App.Current.Logger.LogDebug("ReadAloud: Focused element does not support selected text via UI automation."); } else { - App.Current.Logger.LogDebug("ReadAloud: Selected element is not text."); + captureTextViaAutomationSucceeded = true; + + if (selectedText == String.Empty) + { + App.Current.Logger.LogDebug("ReadAloud: Captured empty selection via UI automation."); + } + else + { + App.Current.Logger.LogDebug("ReadAloud: Captured selected (non-empty) text via UI automation."); + } } } else { - App.Current.Logger.LogDebug("ReadAloud: No element is currently selected."); + // NOTE: we only log errors here, rather than returning an error condition to our caller; we don't return immediately here because we have a backup strategy (i.e. ctrl+c) which we employ if this strategy fails + switch (getSelectedTextResult.Error!.Value) + { + case WindowsNative.UIAutomation.UIAutomationClient.CaptureSelectedTextError.Values.ComInterfaceInstantiationFailed: + App.Current.Logger.LogDebug("ReadAloud: Capture selected text via UI automation failed (com interface could not be instantiated)"); + break; + case WindowsNative.UIAutomation.UIAutomationClient.CaptureSelectedTextError.Values.TextRangeIsNull: + App.Current.Logger.LogDebug("ReadAloud: Capture selected text via UI automation returned a null text range; this is an unexpected error condition"); + break; + case WindowsNative.UIAutomation.UIAutomationClient.CaptureSelectedTextError.Values.Win32Error: + App.Current.Logger.LogDebug("ReadAloud: Capture selected text via UI automation resulted in win32 error code: " + getSelectedTextResult.Error!.Win32ErrorCode.ToString()); + break; + default: + throw new MorphicUnhandledErrorException(); + } } } finally { s_captureTextSemaphore.Release(); } - // - // if we just captured a text range collection (i.e. were able to copy the current selection), convert that capture into a string now - StringBuilder? selectedTextBuilder = null; - if (textRangeCollection is not null) - { - // we have captured a range (presumably either an empty or non-empty selection) - selectedTextBuilder = new StringBuilder(); - - // append each text range - foreach (var textRange in textRangeCollection) - { - if (textRange is not null) - { - selectedTextBuilder.Append(textRange.GetText(-1 /* maximumRange */)); - } - } - //if (selectedTextBuilder is not null /* && stringBuilder.Length > 0 */) - //{ - selectedText = selectedTextBuilder.ToString(); - captureTextViaAutomationSucceeded = true; - - if (selectedText != String.Empty) - { - App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); - } - //} - } + /* Capture strategy #2 (backup strategy): capture text via copy key sequence (i.e. send "ctrl+c" to foreground/last-active window) */ // as a backup strategy, use the clipboard and send ctrl+c to the target window to capture the text contents (while preserving as much of the previous // clipboard's contents as possible); this is necessary in Internet Explorer and some other programs + bool captureTextViaCopyKeySequenceSucceeded = false; if (captureTextViaAutomationSucceeded == false) { + App.Current.Logger.LogDebug("ReadAloud: Capturing selected text via copy key sequence."); + // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block await s_captureTextSemaphore.WaitAsync(); // try { - // App.Current.Logger.LogDebug("ReadAloud: Attempting to back up current clipboard."); - - Dictionary clipboardContentsToRestore = new Dictionary(); - - var previousClipboardData = Clipboard.GetDataObject(); - if (previousClipboardData is not null) + // capture the selected text from the last active (foreground) window + var getSelectedTextFromForegroundWindowResult = await Functions.GetSelectedTextFromForegroundWindowAsync(); + if (getSelectedTextFromForegroundWindowResult.IsError == true) { - // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to capture format(s) of contents."); - string[]? previousClipboardFormats = previousClipboardData.GetFormats(); - if (previousClipboardFormats is not null) - { - // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to back up current clipboard."); - - foreach (var format in previousClipboardFormats) - { - object? dataObject; - try - { - dataObject = previousClipboardData.GetData(format, false /* autoConvert */); - } - catch - { - // NOTE: in the future, we should look at using Project Reunion to use the UWP APIs (if they can deal with this scenario better) - // see: https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.clipboard?view=winrt-19041 - // see: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance - App.Current.Logger.LogDebug("ReadAloud: Unable to back up clipboard contents; this can happen with files copied to the clipboard, etc."); - - return MorphicResult.ErrorResult(); - } - clipboardContentsToRestore[format] = dataObject; - } - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents, but we were unable to obtain their formats."); - } + App.Current.Logger.LogDebug("ReadAloud: Capture selected text via ctrl+c failed."); + captureTextViaCopyKeySequenceSucceeded = false; } else { - App.Current.Logger.LogDebug("ReadAloud: Current clipboard has no contents."); - } - - // clear the current clipboard - App.Current.Logger.LogDebug("ReadAloud: Clearing the current clipboard."); - try - { - // try to clear the clipboard for up to 500ms (4 delays of 125ms) - await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125)); - } - catch - { - App.Current.Logger.LogDebug("ReadAloud: Could not clear the current clipboard."); - } - - // copy the current selection to the clipboard - App.Current.Logger.LogDebug("ReadAloud: Sending Ctrl+C to copy the current selection to the clipboard."); - await SelectionReader.Default.GetSelectedTextAsync(System.Windows.Forms.SendKeys.SendWait); - - // wait 100ms (an arbitrary amount of time, but in our testing some wait is necessary...even with the WM-triggered copy logic above) - // NOTE: perhaps, in the future, we should only do this if our first call to Clipboard.GetText() returns (null? or) an empty string; - // or perhaps we should wait up to a certain number of milliseconds to receive a SECOND WM (the one that GetSelectedTextAsync - // waited for). - await Task.Delay(100); - - // capture the current selection - var selectionWasCopiedToClipboard = false; - var textCopiedToClipboard = Clipboard.GetText(); - if (textCopiedToClipboard is not null) - { - selectionWasCopiedToClipboard = true; - - // we now have our selected text - selectedText = textCopiedToClipboard; + captureTextViaCopyKeySequenceSucceeded = true; + selectedText = getSelectedTextFromForegroundWindowResult.Value; if (selectedText is not null) { - App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); - } - else - { - App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); - } - } - else - { - var copiedDataFormats = Clipboard.GetDataObject()?.GetFormats(); - if (copiedDataFormats is not null) - { - selectionWasCopiedToClipboard = true; - - // var formatsCsvBuilder = new StringBuilder(); - // formatsCsvBuilder.Append("["); - // if (copiedDataFormats.Length > 0) - // { - // formatsCsvBuilder.Append("\""); - // formatsCsvBuilder.Append(String.Join("\", \"", copiedDataFormats)); - // formatsCsvBuilder.Append("\""); - // } - // formatsCsvBuilder.Append("]"); - - // App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy text; instead it copied data in these format(s): " + formatsCsvBuilder.ToString()); - App.Current.Logger.LogDebug("ReadAloud: Ctrl+C copied non-text (un-speakable) contents to the clipboard."); + App.Current.Logger.LogDebug("ReadAloud: Captured selected text via ctrl+c."); } else { - App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy anything to the clipboard."); + App.Current.Logger.LogDebug("ReadAloud: Captured empty selection via ctrl+c."); } - } - // restore the previous clipboard's contents - // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore the previous clipboard's contents"); - // - if (selectionWasCopiedToClipboard == true) - { - // App.Current.Logger.LogDebug("ReadAloud: Clearing the selected text from the clipboard."); - try - { - // try to clear the clipboard for up to 500ms (4 delays of 125ms) - await Functions.ClearClipboardAsync(5, new TimeSpan(0,0,0,0,125)); - } - catch - { - App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); - } } - // - if (clipboardContentsToRestore.Count > 0) - { - // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore " + clipboardContentsToRestore.Count.ToString() + " item(s) to the clipboard."); - } - else - { - // App.Current.Logger.LogDebug("ReadAloud: there is nothing to restore to the clipboard."); - } - // - foreach (var (format, data) in clipboardContentsToRestore) - { - // NOTE: sometimes, data is null (which is not something that SetData can accept) so we have to just skip that element - if (data is not null) - { - Clipboard.SetData(format, data); - } - } - // - App.Current.Logger.LogDebug("ReadAloud: Clipboard restoration complete"); } finally { s_captureTextSemaphore.Release(); } } + + if (captureTextViaAutomationSucceeded == false && captureTextViaCopyKeySequenceSucceeded == false) + { + App.Current.Logger.LogDebug("ReadAloud: Could not capture selected text by automation; could not capture selected text via ctrl+c."); + + // if we were unable to capture the text via either automation or emulating a "copy key sequence (ctrl+c)", then return failure + return MorphicResult.ErrorResult(); + } } catch (Exception ex) { @@ -519,6 +391,8 @@ public static async Task> ReadAloudAsync return MorphicResult.ErrorResult(); } + // NOTE: if we reach here, we were able to capture the selected text (either + if (selectedText is not null) { if (selectedText != String.Empty) @@ -551,16 +425,476 @@ public static async Task> ReadAloudAsync return MorphicResult.OkResult(); } } else { - // could not capture any text - // App.Current.Logger.LogError("ReadAloud: Could not capture any selected text; this may or may not be an error."); + // no text was selected + App.Current.Logger.LogDebug("ReadAloud: No text was selected (or the selected text didn't support capture via UI automation or copying via ctrl+c)."); - return MorphicResult.ErrorResult(); + return MorphicResult.OkResult(); } default: throw new Exception("invalid code path"); } } + // + + private static async Task> GetSelectedTextFromForegroundWindowAsync() + { + string? selectedText = null; + var isSuccess = true; + + /* PHASE 1: capture the newest item in the windows clipboard history (so we can rollback upon completion); we will also _first_ try to directly capture the current clipboard contents as a fallback position (in case of enterprise-disabled clipboard history or failure during clipboard history restoration) */ + + // capture the clipboard's current content directly; ideally we'd use the clipboard history, but sometimes clipboard history isn't available + // + // this variable is used to indicate that we captured the clipboard content directly (in case we need to try to manually restore back to the clipboard content) + bool clipboardContentIsCaptured; + // this variable is used to store the clipboard content + List<(string, object)>? clipboardContent = null; + // + var backupContentResult = await Morphic.WindowsNative.Clipboard.Clipboard.BackupContentAsync(); + if (backupContentResult.IsError == true) + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Unable to back up clipboard content; this can happen when the clipboard content is a file (i.e. filestream), etc."); + clipboardContentIsCaptured = false; + } + else + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Captured current clipboard content"); + clipboardContent = backupContentResult.Value; + clipboardContentIsCaptured = true; + } + // + // NOTE: clipboard content can technically be empty + if (clipboardContent is not null && clipboardContent!.Count == 0) + { + clipboardContent = null; + } + if (clipboardContent is null) + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Current clipboard had no content."); + } + // + + // NOTE: if clipboard content capture fails, we would ideally turn on clipboard history temporarily to use that; unfortunately, Windows does _not_ copy the current clipboard contents to the clipboard history at the time + // that clipboard history is enabled...so we would not be able to roll back to the previous entry using clipboard history + + // capture the last item in the windows clipboard history + // + // this variable is used to indicate that we captured the newest clipboard history item (and should therefore restore back to THAT item) + bool clipboardHistoryIsCaptured; + // this variable is used to store the newest clipboard history item (if the clipboard history was not empty) + Windows.ApplicationModel.DataTransfer.ClipboardHistoryItem? newestClipboardHistoryItem = null; + // + // NOTE: if clipboard history is disabled, we'll capture the "ClipboardHistoryDisabled" error here and fall back to the manual backup of the clipboard content + var getNewestClipboardClipboardHistoryItemResult = await Functions.GetNewestClipboardHistoryItemAsync(); + if (getNewestClipboardClipboardHistoryItemResult.IsError == true) + { + clipboardHistoryIsCaptured = false; + // + switch (getNewestClipboardClipboardHistoryItemResult.Error!.Value) + { + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.AccessDenied: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not get clipboard history (error: access denied)."); + break; + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.ClipboardHistoryDisabled: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Clipboard history is disabled."); + break; + default: + throw new MorphicUnhandledErrorException(); + } + } + else + { + clipboardHistoryIsCaptured = true; + // + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Captured clipboard history."); + newestClipboardHistoryItem = getNewestClipboardClipboardHistoryItemResult.Value!; + } + + if (clipboardContentIsCaptured == false && clipboardHistoryIsCaptured == false) + { + // NOTE: ideally, we would return this problem scenario to the caller so that they could alert the user; unfortunately there aren't any particularly good workarounds (other than perhaps re-implementing the win32 wrappers around clipboard APIs to try to capture the clipboard state perfectly) + // we might also want to create an option in the arguments for this function which allowed the caller to request that we FAIL if the clipboard couldn't be backed up. + // NOTE: clipboard history doesn't appear to backup all "copy" operations (such as file copies); for that reason, clipboard history may be useless to us in most/all scenarios. However we need to support it so that users with clipboard history enabled don't get history entries (i.e. so that Morphic doesn't add each read-aloud text to their clipboard history). + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Clipboard content could not be backed up nor a clipboard history snapshot created; current clipboard content will be cleared."); + } + + // NOTE: we use a try...finally block here to make sure that we restore the clipboard history after our operation is complete + try + { + /* PHASE 2: capture the selected text in the last active ("foreground") window */ + + // capture the selected text + var copySelectedTextFromLastActiveWindowResult = await Functions.CopySelectedTextFromLastActiveWindowAsync(); + if (copySelectedTextFromLastActiveWindowResult.IsError == true) + { + // NOTE: in the future, we may want to consider reporting more robust error information + return MorphicResult.ErrorResult(); + } + selectedText = copySelectedTextFromLastActiveWindowResult.Value; + } + finally + { + /* PHASE 3: restore the original clipboard contents */ + + // if we captured the clipboard history, restore it now + bool clipboardHistoryWasRestored = false; + if (clipboardHistoryIsCaptured == true) + { + if (newestClipboardHistoryItem is not null) + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Attempting to restore the clipboard history."); + var restoreClipboardToHistoryItemResult = await Functions.RollbackClipboardToHistoryItemAsync(newestClipboardHistoryItem); + if (restoreClipboardToHistoryItemResult.IsError == true) + { + switch (restoreClipboardToHistoryItemResult.Error!.Value) + { + case RollbackClipboardToHistoryItemError.Values.AccessDenied: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not restore (rollback) clipboard history: access denied."); + break; + case RollbackClipboardToHistoryItemError.Values.ClipboardHistoryDisabled: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not restore (rollback) clipboard history: clipboard history disabled (although it was enabled at start of capture)."); + break; + case RollbackClipboardToHistoryItemError.Values.CouldNotDeleteItem: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not restore (rollback) clipboard history: could not delete newer items."); + break; + case RollbackClipboardToHistoryItemError.Values.ItemWasDeleted: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not restore (rollback) clipboard history: rollback point item was already deleted (although it existed at start of capture)."); + break; + case RollbackClipboardToHistoryItemError.Values.NoItemsInClipboardHistory: + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not restore (rollback) clipboard history: clipboard history is empty (and therefore the rollback point item is missing)."); + break; + default: + throw new MorphicUnhandledErrorException(); + } + // + // NOTE: in the future, we may want to return an error of "could not roll back history" so that the caller can try to fix the clipboard in another manner. + clipboardHistoryWasRestored = false; + } + else + { + var numberOfClipboardEntriesErased = restoreClipboardToHistoryItemResult.Value!; +#if DEBUG + if (numberOfClipboardEntriesErased != 1) + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: During clipboard history restore, expected one new history item; found " + numberOfClipboardEntriesErased.ToString() + " items instead."); + } +#endif // DEBUG + + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Clipboard history restoration complete"); + clipboardHistoryWasRestored = true; + } + } + else + { + // as there were no entries in the clipboard history, simply clear the history after our copy operation + var clearHistoryResult = Morphic.WindowsNative.Clipboard.Clipboard.ClearHistory(); + if (clearHistoryResult.IsError == true) + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Could not clear clipboard history during clipboard history restore; clearing current contents instead."); + + // in an attempt to gracefully degrade, clear the current contents of the clipboard instead + Morphic.WindowsNative.Clipboard.Clipboard.ClearContent(); + + clipboardHistoryWasRestored = true; + } + else + { + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: there is no clipboard history to restore."); + clipboardHistoryWasRestored = true; + } + } + } + + var clipboardContentWasRestored = false; + if (clipboardHistoryWasRestored == false) + { + // if the clipboard history was not restored (either because there was no support for clipboard history or because clipboard history restoration failed), try to restore the backed-up clipboard content instead + + if (clipboardContentIsCaptured == true) + { + // if the clipboard content was backed up (instead of the history), restore that content now + if (clipboardContent is not null) + { + // if the clipboard was not empty, restore its content + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: Attempting to restore " + clipboardContent.Count.ToString() + " item component(s) to the clipboard."); + Morphic.WindowsNative.Clipboard.Clipboard.RestoreContent(clipboardContent); + } + else + { + // if the clipboard was empty, clear it + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: there is nothing to restore to the clipboard."); + Morphic.WindowsNative.Clipboard.Clipboard.ClearContent(); + } + clipboardContentWasRestored = true; + } + } + + var clipboardContentWasClearedAsBackupPlan = false; + if (clipboardHistoryIsCaptured == false && clipboardContentIsCaptured == false) + { + // NOTE: in this scenario, there is nothing we can do to restore the clipboard, so we just clear it. + App.Current.Logger.LogDebug("GetSelectedTextFromForegroundWindowAsync: the clipboard was not backed up, so we are clearing it."); + Morphic.WindowsNative.Clipboard.Clipboard.ClearContent(); + + clipboardContentWasClearedAsBackupPlan = true; + } + + // if we couldn't restore the clipboard history OR the clipboard content (and we didn't execute an alternate backup plan), then return a failure condition + if (clipboardHistoryWasRestored == false && clipboardContentWasRestored == false && clipboardContentWasClearedAsBackupPlan == false) + { + // NOTE: in the future, we may want to find a way to return this as a warning (while still returning the captured text) rather than simply indicating that an error occurred + isSuccess = false; + } + } + + return isSuccess ? MorphicResult.OkResult(selectedText) : MorphicResult.ErrorResult(); + } + + // NOTE: this function returns null if the clipboard history is empty + private static async Task> GetNewestClipboardHistoryItemAsync() + { + var isClipboardHistoryEnabled = Morphic.WindowsNative.Clipboard.Clipboard.IsHistoryEnabled(); + if (isClipboardHistoryEnabled == false) + { + return MorphicResult.ErrorResult(Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.ClipboardHistoryDisabled); + } + + // NOTE: if we reach here, clipboard history is enabled + + // capture clipboard history + var getHistoryItemsResult = await Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsAsync(); + if (getHistoryItemsResult.IsError == true) + { + switch (getHistoryItemsResult.Error!.Value) + { + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.AccessDenied: + return MorphicResult.ErrorResult(getHistoryItemsResult.Error!); + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.ClipboardHistoryDisabled: + return MorphicResult.ErrorResult(getHistoryItemsResult.Error!); + default: + throw new MorphicUnhandledErrorException(); + } + } + var clipboardHistoryItems = getHistoryItemsResult.Value!; + + Windows.ApplicationModel.DataTransfer.ClipboardHistoryItem? newestClipboardHistoryItem = null; + if (clipboardHistoryItems.Count > 0) + { + newestClipboardHistoryItem = clipboardHistoryItems.OrderBy(item => item.Timestamp).ToList().Last(); + } + else + { + newestClipboardHistoryItem = null; + } + + return MorphicResult.OkResult(newestClipboardHistoryItem); + } + + public static async Task> WaitForClipboardHistoryToIncludeNewItems(DateTimeOffset afterTimestamp, int minimumCount = 1) + { + const int NUMBER_OF_ATTEMPTS = 5; + const int MILLISECONDS_BETWEEN_ATTEMPTS = 50; + + for (var iAttempt = 0; iAttempt < NUMBER_OF_ATTEMPTS; iAttempt += 1) + { + var getHistoryItemsResult = await Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsAsync(); + if (getHistoryItemsResult.IsError == false) + { + var clipboardHistoryItems = getHistoryItemsResult.Value!; + + // get a list of any clipboard history items added since 'afterDateTimeOffset' + var newClipboardHistoryItems = clipboardHistoryItems.OrderBy(x => x.Timestamp).Where(x => x.Timestamp > afterTimestamp).ToList(); + if (newClipboardHistoryItems.Count >= minimumCount) + { + return MorphicResult.OkResult(newClipboardHistoryItems.Count); + } + } + + // if we have not encountered any new items yet, wait MILLISECONDS_BETWEEN_ATTEMPTS and try again + if (iAttempt < NUMBER_OF_ATTEMPTS - 1) + { + await Task.Delay(MILLISECONDS_BETWEEN_ATTEMPTS); + } + } + + // if we did not find any new items before timing out completely, return an error condition + return MorphicResult.ErrorResult(); + } + + public record RollbackClipboardToHistoryItemError : MorphicAssociatedValueEnum + { + // enum members + public enum Values + { + AccessDenied, + ClipboardHistoryDisabled, + CouldNotDeleteItem, + ItemWasDeleted, + NoItemsInClipboardHistory, + } + + // functions to create member instances + public static RollbackClipboardToHistoryItemError AccessDenied => new RollbackClipboardToHistoryItemError(Values.AccessDenied); + public static RollbackClipboardToHistoryItemError ClipboardHistoryDisabled => new RollbackClipboardToHistoryItemError(Values.ClipboardHistoryDisabled); + public static RollbackClipboardToHistoryItemError CouldNotDeleteItem => new RollbackClipboardToHistoryItemError(Values.CouldNotDeleteItem); + public static RollbackClipboardToHistoryItemError ItemWasDeleted => new RollbackClipboardToHistoryItemError(Values.ItemWasDeleted); + public static RollbackClipboardToHistoryItemError NoItemsInClipboardHistory => new RollbackClipboardToHistoryItemError(Values.NoItemsInClipboardHistory); + + // associated values + + // verbatim required constructor implementation for MorphicAssociatedValueEnums + private RollbackClipboardToHistoryItemError(Values value) : base(value) { } + } + // + // NOTE: this function returns the number of items removed from the clipboard history while rolling back to the specified history item + private static async Task> RollbackClipboardToHistoryItemAsync(Windows.ApplicationModel.DataTransfer.ClipboardHistoryItem item) + { + // wait for the clipboard history to include the new items; sometimes it takes the operating system a few moments to catch up + // NOTE: this function uses a somewhat arbitrary countdown time; it may or may not be enough in practice + var waitForClipboardHistoryToIncludeNewItemsResult = await Functions.WaitForClipboardHistoryToIncludeNewItems(item.Timestamp, 1); + Debug.Assert(waitForClipboardHistoryToIncludeNewItemsResult.IsSuccess, "Clipboard history does not yet include the expected new items"); + // NOTE: for now, we ignore the result value + // var numberOfNewClipboardHistoryItems = waitForClipboardHistoryToIncludeNewItemsResult.Value!; + + // delete all entries newer than the supplied item + var getHistoryItemsResult = await Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsAsync(); + if (getHistoryItemsResult.IsError == true) + { + switch (getHistoryItemsResult.Error!.Value) + { + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.AccessDenied: + App.Current.Logger.LogDebug("RollbackClipboardToHistoryItemAsync: Could not get clipboard history during clipboard history restore (error: access denied)."); + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.AccessDenied); + case Morphic.WindowsNative.Clipboard.Clipboard.GetHistoryItemsError.Values.ClipboardHistoryDisabled: + App.Current.Logger.LogDebug("RollbackClipboardToHistoryItemAsync: Could not get clipboard history during clipboard history restore (error: clipboard history disabled)."); + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.ClipboardHistoryDisabled); + default: + throw new MorphicUnhandledErrorException(); + } + } + var clipboardHistoryItems = getHistoryItemsResult.Value!; + + int numberOfHistoryItemsRemoved; + if (clipboardHistoryItems.Count > 0) + { + // get a list of any clipboard history items added since our operation continued; this should just include the clipboard history item we created + var newClipboardHistoryItems = clipboardHistoryItems.OrderBy(x => x.Timestamp).Where(x => x.Timestamp > item.Timestamp).ToList(); + numberOfHistoryItemsRemoved = newClipboardHistoryItems.Count; + + foreach (var newClipboardHistoryItem in newClipboardHistoryItems) + { + var deleteItemFromHistoryResult = Morphic.WindowsNative.Clipboard.Clipboard.DeleteItemFromHistory(newClipboardHistoryItem); + if (deleteItemFromHistoryResult.IsError == true) + { + App.Current.Logger.LogDebug("RollbackClipboardToHistoryItemAsync: Could not delete new history item during clipboard history restore (error: could not delete item from history)."); + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.CouldNotDeleteItem); + } + } + } + else + { + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.NoItemsInClipboardHistory); + } + + // restore the previously-newest clipboard entry + var setHistoryItemAsContentResult = Morphic.WindowsNative.Clipboard.Clipboard.SetHistoryItemAsContent(item); + if (setHistoryItemAsContentResult.IsError == true) + { + switch (setHistoryItemAsContentResult.Error!.Value) + { + case WindowsNative.Clipboard.Clipboard.SetHistoryItemAsContentError.Values.AccessDenied: + App.Current.Logger.LogDebug("RollbackClipboardToHistoryItemAsync: Could not restore history setpoint during clipboard history restore (error: access denied)."); + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.AccessDenied); + case WindowsNative.Clipboard.Clipboard.SetHistoryItemAsContentError.Values.ItemDeleted: + App.Current.Logger.LogDebug("RollbackClipboardToHistoryItemAsync: Could not restore history setpoint during clipboard history restore (error: item deleted)."); + return MorphicResult.ErrorResult(RollbackClipboardToHistoryItemError.ItemWasDeleted); + default: + throw new MorphicUnhandledErrorException(); + } + } + + return MorphicResult.OkResult(numberOfHistoryItemsRemoved); + } + + private static async Task> CopySelectedTextFromLastActiveWindowAsync() + { + // clear the clipboard content before copying the text (in case the target application didn't have anything to copy) + Morphic.WindowsNative.Clipboard.Clipboard.ClearContent(); + + // NOTE: previously, we called ClearClipboardAsync and got an error back if the clipboard couldn't be cleared; in our current implementation, we assume that the ClearContent function will always succeed synchronously + //try + //{ + // // try to clear the clipboard for up to 500ms (4 delays of 125ms) + // await Functions.ClearClipboardAsync(5, new TimeSpan(0,0,0,0,125)); + //} + //catch + //{ + // App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindowAsync: Could not clear selected text from the clipboard."); + //} + + // copy the current selection to the clipboard + App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindow: Sending Ctrl+C to copy the current selection to the clipboard."); + await SelectionReader.Default.CopySelectedTextToClipboardAsync(System.Windows.Forms.SendKeys.SendWait); + + // wait 100ms (an arbitrary amount of time, but in our testing some wait is necessary...even with the WM-triggered copy logic above) + // NOTE: perhaps, in the future, we should only do this if our first call to Clipboard.GetText() returns (null? or) an empty string; + // or perhaps we should wait up to a certain number of milliseconds to receive a SECOND WM (the one that GetSelectedTextAsync + // waited for). + await Task.Delay(100); + + // capture the current selection + var textCopiedToClipboard = await Morphic.WindowsNative.Clipboard.Clipboard.GetTextAsync(); + if (textCopiedToClipboard is not null) + { + // we now have our selected text + string? selectedText = textCopiedToClipboard; + + if (selectedText is not null) + { + App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindow: Captured selected text."); + } + else + { + App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindow: Captured empty selection."); + } + + return MorphicResult.OkResult(selectedText); + } + else + { + // write out diagnostics information if we were unable to copy contents via ctrl+c + + var copiedDataFormats = Morphic.WindowsNative.Clipboard.Clipboard.GetContentFormats(); + if (copiedDataFormats is not null && copiedDataFormats.Count > 0) + { + // var formatsCsvBuilder = new StringBuilder(); + // formatsCsvBuilder.Append("["); + // if (copiedDataFormats.Length > 0) + // { + // formatsCsvBuilder.Append("\""); + // formatsCsvBuilder.Append(String.Join("\", \"", copiedDataFormats)); + // formatsCsvBuilder.Append("\""); + // } + // formatsCsvBuilder.Append("]"); + + // App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy text; instead it copied data in these format(s): " + formatsCsvBuilder.ToString()); + App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindow: Ctrl+C copied non-text (nonspeakable) contents to the clipboard."); + } + else + { + App.Current.Logger.LogDebug("CopySelectedTextFromLastActiveWindow: Ctrl+C did not copy anything to the clipboard."); + + // NOTE: we are making an assumption here that nothing was selected if we executed "copy" and the clipboard was subsequently empty + return MorphicResult.OkResult(null); + } + + return MorphicResult.ErrorResult(); + } + } + + // + /// /// Sends key strokes to the active application. /// diff --git a/Morphic.Client/Morphic.Client.csproj b/Morphic.Client/Morphic.Client.csproj index daac801d..07df5a96 100644 --- a/Morphic.Client/Morphic.Client.csproj +++ b/Morphic.Client/Morphic.Client.csproj @@ -83,8 +83,8 @@ - - + + diff --git a/Morphic.WindowsNative/Clipboard/Clipboard.cs b/Morphic.WindowsNative/Clipboard/Clipboard.cs new file mode 100644 index 00000000..68126333 --- /dev/null +++ b/Morphic.WindowsNative/Clipboard/Clipboard.cs @@ -0,0 +1,263 @@ +// Copyright 2022 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Morphic.WindowsNative.Clipboard +{ + public class Clipboard + { + public static bool IsHistoryEnabled() + { + return Windows.ApplicationModel.DataTransfer.Clipboard.IsHistoryEnabled(); + } + + public static MorphicResult SetHistoryEnabled(bool value) + { + var openRegistryKeyResult = Morphic.WindowsNative.Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Clipboard", true); + if (openRegistryKeyResult.IsError == true) + { + return MorphicResult.ErrorResult(); + } + var registryKey = openRegistryKeyResult.Value!; + + var setValueResult = registryKey.SetValue("EnableClipboardHistory", value ? (uint)1 : (uint)0); + if (setValueResult.IsError == true) + { + return MorphicResult.ErrorResult(); + } + + return MorphicResult.OkResult(); + } + + public record BackupClipboardError : MorphicAssociatedValueEnum + { + // enum members + public enum Values + { + UnhandledException, + Win32Error, + } + + // functions to create member instances + public static BackupClipboardError UnhandledException(Exception exception) => new BackupClipboardError(Values.UnhandledException) { Exception = exception }; + public static BackupClipboardError Win32Error(int win32ErrorCode) => new BackupClipboardError(Values.Win32Error) { Win32ErrorCode = win32ErrorCode }; + + // associated values + public Exception? Exception { get; private set; } + public int? Win32ErrorCode { get; private set; } + + // verbatim required constructor implementation for MorphicAssociatedValueEnums + private BackupClipboardError(Values value) : base(value) { } + } + // + public static async Task?, BackupClipboardError>> BackupContentAsync() + { + var clipboardContentView = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); + var clipboardAvailableFormats = clipboardContentView.AvailableFormats; + // + if (clipboardAvailableFormats is null) + { + return MorphicResult.OkResult?>(null); + } + // + List<(string, object)> clipboardContents = new(); + foreach (var availableFormatId in clipboardAvailableFormats) + { + object content; + try + { + content = await clipboardContentView.GetDataAsync(availableFormatId); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(BackupClipboardError.Win32Error(ex.ErrorCode)); + } + catch (Exception ex) + { + return MorphicResult.ErrorResult(BackupClipboardError.UnhandledException(ex)); + } + clipboardContents.Add((availableFormatId, content)); + } + + return MorphicResult.OkResult?>(clipboardContents); + } + + public static void RestoreContent(List<(string, object)> content) + { + // NOTE: we clear the content before restoring new content to make sure that we don't mix new clipboard data entries with existing clipboard data entries + Windows.ApplicationModel.DataTransfer.Clipboard.Clear(); + + var clipboardContent = new Windows.ApplicationModel.DataTransfer.DataPackage(); + foreach ((string formatId, object data) in content) + { + clipboardContent.SetData(formatId, data); + } + + //Windows.ApplicationModel.DataTransfer.Clipboard.SetContentWithOptions(dataPackageToRestore, new Windows.ApplicationModel.DataTransfer.ClipboardContentOptions() { IsAllowedInHistory = false }); + Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(clipboardContent); + } + + public static void ClearContent() + { + Windows.ApplicationModel.DataTransfer.Clipboard.Clear(); + } + + public static async Task GetTextAsync() + { + var clipboardContentView = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); + // + var clipboardAvailableFormats = clipboardContentView.AvailableFormats; + if (clipboardAvailableFormats is null) + { + return null; + } + // + foreach (var availableFormat in clipboardAvailableFormats!) + { + if (availableFormat == Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text) + { + return await clipboardContentView.GetTextAsync(); + } + } + + // if we did not find a text representation, return null + return null; + } + + public static List? GetContentFormats() + { + var clipboardContentView = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent(); + // + var clipboardAvailableFormats = clipboardContentView.AvailableFormats; + if (clipboardAvailableFormats is null) + { + return null; + } + + var contentFormats = new List(); + foreach (var availableFormat in clipboardAvailableFormats!) + { + contentFormats.Add(availableFormat); + } + // + return contentFormats; + } + + public record GetHistoryItemsError : MorphicAssociatedValueEnum + { + // enum members + public enum Values + { + AccessDenied, + ClipboardHistoryDisabled, + } + + // functions to create member instances + public static GetHistoryItemsError AccessDenied => new GetHistoryItemsError(Values.AccessDenied); + public static GetHistoryItemsError ClipboardHistoryDisabled => new GetHistoryItemsError(Values.ClipboardHistoryDisabled); + + // associated values + + // verbatim required constructor implementation for MorphicAssociatedValueEnums + private GetHistoryItemsError(Values value) : base(value) { } + } + // + public static async Task, GetHistoryItemsError>> GetHistoryItemsAsync() + { + var getHistoryItemsResult = await Windows.ApplicationModel.DataTransfer.Clipboard.GetHistoryItemsAsync(); + switch (getHistoryItemsResult.Status) + { + case Windows.ApplicationModel.DataTransfer.ClipboardHistoryItemsResultStatus.AccessDenied: + return MorphicResult.ErrorResult(GetHistoryItemsError.AccessDenied); + case Windows.ApplicationModel.DataTransfer.ClipboardHistoryItemsResultStatus.ClipboardHistoryDisabled: + return MorphicResult.ErrorResult(GetHistoryItemsError.ClipboardHistoryDisabled); + case Windows.ApplicationModel.DataTransfer.ClipboardHistoryItemsResultStatus.Success: + // success + break; + default: + throw new MorphicUnhandledErrorException(); + } + + var result = new List(); + foreach (var clipboardHistoryItem in getHistoryItemsResult.Items) + { + result.Add(clipboardHistoryItem); + } + return MorphicResult.OkResult(result); + } + + + public record SetHistoryItemAsContentError : MorphicAssociatedValueEnum + { + // enum members + public enum Values + { + AccessDenied, + ItemDeleted, + } + + // functions to create member instances + public static SetHistoryItemAsContentError AccessDenied => new SetHistoryItemAsContentError(Values.AccessDenied); + public static SetHistoryItemAsContentError ItemDeleted => new SetHistoryItemAsContentError(Values.ItemDeleted); + + // associated values + + // verbatim required constructor implementation for MorphicAssociatedValueEnums + private SetHistoryItemAsContentError(Values value) : base(value) { } + } + // + public static MorphicResult SetHistoryItemAsContent(Windows.ApplicationModel.DataTransfer.ClipboardHistoryItem item) + { + var setHistoryItemAsContentResult = Windows.ApplicationModel.DataTransfer.Clipboard.SetHistoryItemAsContent(item); + switch (setHistoryItemAsContentResult) + { + case Windows.ApplicationModel.DataTransfer.SetHistoryItemAsContentStatus.AccessDenied: + return MorphicResult.ErrorResult(SetHistoryItemAsContentError.AccessDenied); + case Windows.ApplicationModel.DataTransfer.SetHistoryItemAsContentStatus.ItemDeleted: + return MorphicResult.ErrorResult(SetHistoryItemAsContentError.ItemDeleted); + case Windows.ApplicationModel.DataTransfer.SetHistoryItemAsContentStatus.Success: + return MorphicResult.OkResult(); + default: + throw new MorphicUnhandledErrorException(); + } + } + + public static MorphicResult ClearHistory() + { + var success = Windows.ApplicationModel.DataTransfer.Clipboard.ClearHistory(); + return success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); + } + + public static MorphicResult DeleteItemFromHistory(Windows.ApplicationModel.DataTransfer.ClipboardHistoryItem item) + { + var success = Windows.ApplicationModel.DataTransfer.Clipboard.DeleteItemFromHistory(item); + return success ? MorphicResult.OkResult() : MorphicResult.ErrorResult(); + } + } +} diff --git a/Morphic.WindowsNative/Morphic.WindowsNative.csproj b/Morphic.WindowsNative/Morphic.WindowsNative.csproj index 1196ae7e..b142b68a 100644 --- a/Morphic.WindowsNative/Morphic.WindowsNative.csproj +++ b/Morphic.WindowsNative/Morphic.WindowsNative.csproj @@ -5,8 +5,20 @@ enable AnyCPU;x64 + + + tlbimp + 0 + 1 + 944de083-8fb8-45cf-bcb7-c477acb2f897 + 0 + false + false + + + diff --git a/Morphic.WindowsNative/Speech/SelectionReader.cs b/Morphic.WindowsNative/Speech/SelectionReader.cs index b9ca4e8b..6928b424 100644 --- a/Morphic.WindowsNative/Speech/SelectionReader.cs +++ b/Morphic.WindowsNative/Speech/SelectionReader.cs @@ -65,12 +65,12 @@ public void Initialise(IntPtr hwnd) } /// - /// Gets the selected text of the given window, or the last activate window. + /// Gets the selected text of the given window (default is the most recently activated window). /// /// The SendKeys.SendWait method. /// The window. /// The selected text. - public async Task GetSelectedTextAsync(Action sendKeys, IntPtr? windowHandle = null) + public async Task CopySelectedTextToClipboardAsync(Action sendKeys, IntPtr? windowHandle = null) { IntPtr hwnd = windowHandle ?? this.lastWindow; await Task.Run(() => diff --git a/Morphic.WindowsNative/UIAutomation/UIAutomationClient.cs b/Morphic.WindowsNative/UIAutomation/UIAutomationClient.cs new file mode 100644 index 00000000..06edc0ff --- /dev/null +++ b/Morphic.WindowsNative/UIAutomation/UIAutomationClient.cs @@ -0,0 +1,321 @@ +// Copyright 2022 Raising the Floor - US, Inc. +// +// Licensed under the New BSD license. You may not use this file except in +// compliance with this License. +// +// You may obtain a copy of the License at +// https://github.com/raisingthefloor/morphic-windows/blob/master/LICENSE.txt +// +// The R&D leading to these results received funding from the: +// * Rehabilitation Services Administration, US Dept. of Education under +// grant H421A150006 (APCP) +// * National Institute on Disability, Independent Living, and +// Rehabilitation Research (NIDILRR) +// * Administration for Independent Living & Dept. of Education under grants +// H133E080022 (RERC-IT) and H133E130028/90RE5003-01-00 (UIITA-RERC) +// * European Union's Seventh Framework Programme (FP7/2007-2013) grant +// agreement nos. 289016 (Cloud4all) and 610510 (Prosperity4All) +// * William and Flora Hewlett Foundation +// * Ontario Ministry of Research and Innovation +// * Canadian Foundation for Innovation +// * Adobe Foundation +// * Consumer Electronics Association Foundation + +using Morphic.Core; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using UIAutomationClient; + +namespace Morphic.WindowsNative.UIAutomation +{ + public class UIAutomationClient + { + public record CaptureSelectedTextError : MorphicAssociatedValueEnum + { + // enum members + public enum Values + { + ComInterfaceInstantiationFailed, + TextRangeIsNull, + Win32Error, + } + + // functions to create member instances + public static CaptureSelectedTextError ComInterfaceInstantiationFailed => new CaptureSelectedTextError(Values.ComInterfaceInstantiationFailed); + public static CaptureSelectedTextError TextRangeIsNull => new CaptureSelectedTextError(Values.TextRangeIsNull); + public static CaptureSelectedTextError Win32Error(int win32ErrorCode) => new CaptureSelectedTextError(Values.Win32Error) { Win32ErrorCode = win32ErrorCode }; + + // associated values + public int? Win32ErrorCode { get; private set; } + + // verbatim required constructor implementation for MorphicAssociatedValueEnums + private CaptureSelectedTextError(Values value) : base(value) { } + } + // + // NOTE: this function returns an OkResult with a null string? value if the focused window doesn't have a focused element that supports selecting text + public static MorphicResult GetSelectedText() + { + // instantiate our UIAutomation class (via COM interop) + // NOTE: we use the CUIAutomation8 object (which implements IUIAutomation2 through IUIAutomation6); this new interface was introduced in Windows 8 and allows for timeouts, etc.; the latest features of interface IUIAutomation6 are only available in Windows 10 v1809 and newer + CUIAutomation8? comUIAutomation = null; + try + { + comUIAutomation = new CUIAutomation8(); + } + catch + { + // if our COM instantiation failed (e.g. FileNotFound exception), return that error condition to the caller + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + if (comUIAutomation is null) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + try + { + // NOTE: at this point, we know that we have a COM-instantiated CUIAutomation8 instance (which should be compatible with IUIAutomation2 through IUIAutomation6 interfaces on Windows 10 v1809+ and newer) + + var getSelectedTextUsingUIAutomationResult = UIAutomationClient.GetSelectedTextUsingUIAutomation(comUIAutomation!); + if (getSelectedTextUsingUIAutomationResult.IsError == true) + { + return MorphicResult.ErrorResult(getSelectedTextUsingUIAutomationResult.Error!); + } + var selectedText = getSelectedTextUsingUIAutomationResult.Value; + + return MorphicResult.OkResult(selectedText); + } + finally + { + // manually release our COM UIAutomation object and set its manual wrapper reference to null + if (comUIAutomation is not null) + { + Marshal.ReleaseComObject(comUIAutomation); + comUIAutomation = null; + } + } + } + + private static MorphicResult GetSelectedTextUsingUIAutomation(CUIAutomation8 comUIAutomation) + { + // when we obtain our UI element, we need to make sure it supports the text pattern; we specify this via a cache request + IUIAutomationCacheRequest? comUIAutomationCacheRequest = null; + try + { + comUIAutomationCacheRequest = comUIAutomation!.CreateCacheRequest(); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.Win32Error(ex.ErrorCode)); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + if (comUIAutomationCacheRequest is null) + { + Debug.Assert(false, "CUIAutomation8.CreateCacheRequest should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + + try + { + // NOTE: at this point, we know that we have a COM-instantiated IUIAutomationCacheRequest instance (which we can now populate with the cache request information we need to supply when we search for our focused element) + + // identify the Text control pattern in our cache request + // NOTE: although UIA_TextPattern2Id was introduced in Windows 8, we don't need any additional capabilities from this newer pattern; to avoid any (unlikely) app compatibility issues, we're using the earlier (non-extended) interface + comUIAutomationCacheRequest!.AddPattern(UIA_PatternIds.UIA_TextPatternId); + // + // make sure that the Text control pattern is available for the hWnd's automation element (in our cache request) + // NOTE: although UIA_IsTextPattern2AvailablePropertyId was introduced in Windows 8, we don't need any additional capabilities from this newer pattern; to avoid any (unlikely) app compatibility issues, we're using the earlier (non-extended) interface + comUIAutomationCacheRequest!.AddProperty(UIA_PropertyIds.UIA_IsTextPatternAvailablePropertyId); + + // using our cache request properties (i.e. text element), find the focused element; this should be the actual element which we query to retrieve the selected text + IUIAutomationElement? comUIAutomationElement = null; + try + { + comUIAutomationElement = comUIAutomation!.GetFocusedElementBuildCache(comUIAutomationCacheRequest!); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.Win32Error(ex.ErrorCode)); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + // NOTE: in our testing, we once got back "null", but were unable to determine the cause; it _might_ be because we weren't focused on a window which has a document control; we may need to explore this scenario further, add warnings/errors/fallbacks etc. + if (comUIAutomationElement is null) + { + Debug.Assert(false, "CUIAutomationElement.GetFocusedElementBuildCache should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + // + try + { + // NOTE: at this point, we should have the handle of an element which we can query for selected text + + var queryElementForSelectedTextResult = UIAutomationClient.QueryElementForSelectedText(comUIAutomationElement!); + if (queryElementForSelectedTextResult.IsError == true) + { + return MorphicResult.ErrorResult(queryElementForSelectedTextResult.Error!); + } + string? selectedText = queryElementForSelectedTextResult.Value; + + return MorphicResult.OkResult(selectedText); + } + finally + { + if (comUIAutomationElement is not null) + { + Marshal.ReleaseComObject(comUIAutomationElement); + comUIAutomationElement = null; + } + } + } + finally + { + if (comUIAutomationCacheRequest is not null) + { + Marshal.ReleaseComObject(comUIAutomationCacheRequest); + comUIAutomationCacheRequest = null; + } + } + } + + private static MorphicResult QueryElementForSelectedText(IUIAutomationElement comUIAutomationElement) + { + // NOTE: at this point, we should have the handle of an element which we can query for selected text + + // determine if the child element supports the text pattern (so that we can query for selected text) + var isTextPatternAvailable = (bool)comUIAutomationElement.GetCachedPropertyValue(UIA_PropertyIds.UIA_IsTextPatternAvailablePropertyId); + if (isTextPatternAvailable == false) + { + // if the focused element doesn't support the text pattern, return null + return MorphicResult.OkResult(null); + } + + // we now know that the control supports text selected; let's retrieve the selected text + + // create an IUIAutomationTextPattern-compatible object (as an IUnknown) for the element using the text pattern + IUIAutomationTextPattern? comUIAutomationTextPattern = null; + try + { + comUIAutomationTextPattern = (IUIAutomationTextPattern)comUIAutomationElement.GetCachedPattern(UIA_PatternIds.UIA_TextPatternId); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.Win32Error(ex.ErrorCode)); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + if (comUIAutomationTextPattern is null) + { + Debug.Assert(false, "IUIAutomationElement.GetCachedPattern should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + // + try + { + // NOTE: at this point, we have a COM-instantiated IUIAutomationTextPattern-compatible object which we can use to capture text from the current selection + IUIAutomationTextRangeArray? comUIAutomationTextRangeArray = null; + try + { + comUIAutomationTextRangeArray = comUIAutomationTextPattern!.GetSelection(); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.Win32Error(ex.ErrorCode)); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + if (comUIAutomationTextRangeArray is null) + { + Debug.Assert(false, "IUIAutomationTextPattern.GetSelection should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + // + try + { + // NOTE: at this point, we have a COM-instantiated IUIAutomationTextRangeArray (text range array); now let's extract each text range from this array, convert it to a string, and then concatenate all those ranges together into a string to return to our caller + + var extractStringFromTextRangeArrayResult = UIAutomationClient.ExtractStringFromTextRangeArray(comUIAutomationTextRangeArray); + if (extractStringFromTextRangeArrayResult.IsError == true) + { + return MorphicResult.ErrorResult(extractStringFromTextRangeArrayResult.Error!); + } + string? selectedText = extractStringFromTextRangeArrayResult.Value; + + // return the selected text to the caller + return MorphicResult.OkResult(selectedText); + } + finally + { + if (comUIAutomationTextRangeArray is not null) + { + Marshal.ReleaseComObject(comUIAutomationTextRangeArray); + comUIAutomationTextRangeArray = null; + } + } + } + finally + { + if (comUIAutomationTextPattern is not null) + { + Marshal.ReleaseComObject(comUIAutomationTextPattern); + comUIAutomationTextPattern = null; + } + } + } + + private static MorphicResult ExtractStringFromTextRangeArray(IUIAutomationTextRangeArray comUIAutomationTextRangeArray) + { + // NOTE: at this point, we have a COM-instantiated IUIAutomationTextRangeArray (text range array); now let's extract each text range from this array, convert it to a string, and then concatenate all those ranges together into a string to return to our caller + + StringBuilder selectedTextBuilder = new(); + var numberOfTextRanges = comUIAutomationTextRangeArray.Length; + if (numberOfTextRanges > 0) + { + for (var iTextRange = 0; iTextRange < numberOfTextRanges; iTextRange += 1) + { + IUIAutomationTextRange? comUIAutomationTextRange = null; + try + { + comUIAutomationTextRange = comUIAutomationTextRangeArray!.GetElement(iTextRange); + } + catch (COMException ex) + { + return MorphicResult.ErrorResult(CaptureSelectedTextError.Win32Error(ex.ErrorCode)); + } + // if our COM instantiation failed (by returning null, without an exception) then return that error condition to the caller + if (comUIAutomationTextRange is null) + { + Debug.Assert(false, "IUIAutomationTextRangeArray.GetElement should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.ComInterfaceInstantiationFailed); + } + // + try + { + var textRangeAsString = comUIAutomationTextRange!.GetText(-1); + if (textRangeAsString is null) + { + Debug.Assert(false, "IUIAutomationTextRange.GetText should not return null; investigate this unexpected condition (and, if it's an allowed scenario, then simply remove this assertion and return the ComInterfaceInstantiationFailed error."); + return MorphicResult.ErrorResult(CaptureSelectedTextError.TextRangeIsNull); + } + selectedTextBuilder.Append(textRangeAsString); + } + finally + { + if (comUIAutomationTextRange is not null) + { + Marshal.ReleaseComObject(comUIAutomationTextRange); + comUIAutomationTextRange = null; + } + } + } + + // return the selected text to the caller + return MorphicResult.OkResult(selectedTextBuilder.ToString()); + } + else + { + return MorphicResult.OkResult(null); + } + } + } +} From c9fe6d3c5123fb11509cb6f5aa8d7f66a92db0f7 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:35:37 -0700 Subject: [PATCH 06/29] Update build SDK version --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 53a80d6e..dfffeedb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -57,10 +57,10 @@ stages: steps: - task: UseDotNet@2 - displayName: 'Use .NET SDK 5.0.301' + displayName: 'Use .NET SDK 5.0.408' inputs: packageType: sdk - version: 5.0.301 + version: 5.0.408 installationPath: $(Agent.ToolsDirectory)/dotnet # Set a $(BUILD_TYPE) variable From 3baf606e0ef53888bfd32e1abab5cc925e5baa43 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:43:23 -0700 Subject: [PATCH 07/29] Update 'build solutions' step in pipelines --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dfffeedb..5a3ca0a6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -107,6 +107,7 @@ stages: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' + vsVersion: '16.0' msbuildArgs: '/p:BuildType=$(BUILD_TYPE)' - task: VSTest@2 From c84c6df189dfe94d7b2cccd4e6faa377592bed35 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:43:58 -0700 Subject: [PATCH 08/29] Update build SDK version --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5a3ca0a6..76c4c19b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -107,7 +107,7 @@ stages: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - vsVersion: '16.0' + vsVersion: '16.0' msbuildArgs: '/p:BuildType=$(BUILD_TYPE)' - task: VSTest@2 From 1834fdbb5080b27e1c060e462120fcbe87f69053 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 18:59:03 -0700 Subject: [PATCH 09/29] Update build SDK version --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 76c4c19b..56823378 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,7 +108,7 @@ stages: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' vsVersion: '16.0' - msbuildArgs: '/p:BuildType=$(BUILD_TYPE)' + msbuildArgs: '-p:BuildType=$(BUILD_TYPE) -toolsversion:4.0' - task: VSTest@2 displayName: "execute tests" From d5de600492054f0ec845a1bc7fd185ef0f18df75 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 19:03:04 -0700 Subject: [PATCH 10/29] Update 'build solutions' step --- azure-pipelines.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 56823378..289ff405 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -99,7 +99,7 @@ stages: targetType: 'filePath' filePath: set-build-info.sh - - task: VSBuild@1 + - task: MSBuild@1 displayName: "build solutions" env: VERSIONBUILDCOMPONENTS: '$(versionBuildComponents)' @@ -107,8 +107,7 @@ stages: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - vsVersion: '16.0' - msbuildArgs: '-p:BuildType=$(BUILD_TYPE) -toolsversion:4.0' + msbuildArguments: '/p:BuildType=$(BUILD_TYPE)' - task: VSTest@2 displayName: "execute tests" From c638d1ca39994ffec7f6cc1e95564e31ccfd6448 Mon Sep 17 00:00:00 2001 From: Christopher - RtF <58520035+christopher-rtf@users.noreply.github.com> Date: Wed, 31 Aug 2022 19:08:29 -0700 Subject: [PATCH 11/29] Update 'build solutions' step --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 289ff405..dfffeedb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -99,7 +99,7 @@ stages: targetType: 'filePath' filePath: set-build-info.sh - - task: MSBuild@1 + - task: VSBuild@1 displayName: "build solutions" env: VERSIONBUILDCOMPONENTS: '$(versionBuildComponents)' @@ -107,7 +107,7 @@ stages: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - msbuildArguments: '/p:BuildType=$(BUILD_TYPE)' + msbuildArgs: '/p:BuildType=$(BUILD_TYPE)' - task: VSTest@2 displayName: "execute tests" From dc9d1534afea0be57f7d2866bd586b48fe9219c6 Mon Sep 17 00:00:00 2001 From: David Stetz Date: Thu, 1 Sep 2022 17:46:00 -0700 Subject: [PATCH 12/29] Changed pre-build event to use msbuild instead of dotnet command. --- Morphic.Setup/Morphic.Setup.wixproj | 3 +++ MorphicWin.sln | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Morphic.Setup/Morphic.Setup.wixproj b/Morphic.Setup/Morphic.Setup.wixproj index e7f73ea4..70039d2c 100644 --- a/Morphic.Setup/Morphic.Setup.wixproj +++ b/Morphic.Setup/Morphic.Setup.wixproj @@ -69,6 +69,9 @@ dotnet publish $(ProjectDir)..\Morphic.Client\Morphic.Client.csproj -f netcoreapp3.1 -r win-x64 -c $(Configuration) -p:BuildType=$(BuildType) + + "$(MSBuildBinPath)\msbuild.exe" $(ProjectDir)..\Morphic.Client\Morphic.Client.csproj /t:restore,publish /p:RuntimeIdentifier=win-x64 /p:Configuration=$(Configuration) /p:BuildType=$(BuildType) +