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)
+