public static async Task <bool> DarkMode(FunctionArgs args) { bool on = args["state"] == "on"; Setting appSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeApps); await appSetting.SetValue(!on); return(true); }
public static async Task <bool> Screenshot(FunctionArgs args) { // Hide all application windows Dictionary <Window, double> opacity = new Dictionary <Window, double>(); HashSet <Window> visible = new HashSet <Window>(); try { foreach (Window window in App.Current.Windows) { if (window is BarWindow || window is QuickHelpWindow) { if (window.AllowsTransparency) { opacity[window] = window.Opacity; window.Opacity = 0; } else { visible.Add(window); window.Visibility = Visibility.Collapsed; } } } // Give enough time for the windows to disappear await Task.Delay(500); // Hold down the windows key while pressing shift + s const uint windowsKey = 0x5b; // VK_LWIN Morphic.Windows.Native.Keyboard.PressKey(windowsKey, true); System.Windows.Forms.SendKeys.SendWait("+s"); Morphic.Windows.Native.Keyboard.PressKey(windowsKey, false); } finally { // Give enough time for snip tool to grab the screen without the morphic UI. await Task.Delay(3000); // Restore the windows foreach ((Window window, double o) in opacity) { window.Opacity = o; } foreach (Window window in visible) { window.Visibility = Visibility.Visible; } } return(true); }
/// <summary> /// Invokes a built-in function. /// </summary> /// <param name="functionName">The function name.</param> /// <param name="functionArgs">The parameters.</param> /// <returns></returns> public Task <bool> InvokeFunction(string functionName, Dictionary <string, string> functionArgs) { App.Current.Logger.LogDebug($"Invoking built-in function '{functionName}'"); Task <bool> result; if (this.all.TryGetValue(functionName.ToLowerInvariant(), out InternalFunctionAttribute? functionAttribute)) { FunctionArgs args = new FunctionArgs(functionAttribute, functionArgs); result = functionAttribute.Function(args); } else { throw new ActionException($"No internal function found for '{functionName}"); } return(result); }
public static Task <bool> Volume(FunctionArgs args) { IntPtr taskTray = WinApi.FindWindow("Shell_TrayWnd", IntPtr.Zero); if (taskTray != IntPtr.Zero) { int action = args["direction"] == "up" ? WinApi.APPCOMMAND_VOLUME_UP : WinApi.APPCOMMAND_VOLUME_DOWN; // Each command moves the volume by 2 notches. int times = Math.Clamp(Convert.ToInt32(args["amount"]), 1, 20) / 2; for (int n = 0; n < times; n++) { WinApi.SendMessage(taskTray, WinApi.WM_APPCOMMAND, IntPtr.Zero, (IntPtr)WinApi.MakeLong(0, (short)action)); } } return(Task.FromResult(true)); }
public async static Task <MorphicResult <MorphicUnit, MorphicUnit> > ShowMenuAsync(FunctionArgs args) { // NOTE: this internal function is only called by the MorphicBar's Morphie menu button await App.Current.ShowMenuAsync(null, Morphic.Client.Menu.MorphicMenu.MenuOpenedSource.morphicBarIcon); return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > EjectAllUsbDrivesAsync(FunctionArgs args) { App.Current.Logger.LogError("EjectAllUsbDrives"); var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync(); if (getRemovableDisksAndDrivesResult.IsError == true) { Debug.Assert(false, "Could not get list of removable disks"); App.Current.Logger.LogError("Could not get list of removable disks"); return(MorphicResult.ErrorResult()); } var removableDisks = getRemovableDisksAndDrivesResult.Value !.RemovableDisks; // now eject all the removable disks var allDisksRemoved = true; foreach (var disk in removableDisks) { App.Current.Logger.LogError("Safely ejecting drive"); // NOTE: "safe eject" in this circumstance means to safely eject the usb device (removing it from the PnP system, not physically ejecting media) var safeEjectResult = disk.SafelyRemoveDevice(); if (safeEjectResult.IsError == true) { allDisksRemoved = false; } // wait 50ms between ejection await Task.Delay(50); } if (allDisksRemoved == false) { return(MorphicResult.ErrorResult()); } return(allDisksRemoved ? MorphicResult.OkResult() : MorphicResult.ErrorResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > OpenAllUsbDrivesAsync(FunctionArgs args) { App.Current.Logger.LogError("OpenAllUsbDrives"); var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync(); if (getRemovableDisksAndDrivesResult.IsError == true) { Debug.Assert(false, "Could not get list of removable drives"); App.Current.Logger.LogError("Could not get list of removable drives"); return(MorphicResult.ErrorResult()); } var removableDrives = getRemovableDisksAndDrivesResult.Value !.RemovableDrives; // as we only want to open usb drives which are mounted (i.e. not USB drives which have had their "media" ejected but who still have drive letters assigned)... var mountedRemovableDrives = new List <Morphic.WindowsNative.Devices.Drive>(); foreach (var drive in removableDrives) { var getIsMountedResult = await drive.GetIsMountedAsync(); if (getIsMountedResult.IsError == true) { Debug.Assert(false, "Could not determine if drive is mounted"); App.Current.Logger.LogError("Could not determine if drive is mounted"); // gracefully degrade; skip this disk continue; } var driveIsMounted = getIsMountedResult.Value !; if (driveIsMounted) { mountedRemovableDrives.Add(drive); } } // now open all the *mounted* removable disks foreach (var drive in mountedRemovableDrives) { // get the drive's root path (e.g. "E:\"); note that we intentionally get the root path WITH the backslash so that we don't launch autoplay, etc. var tryGetDriveRootPathResult = await drive.TryGetDriveRootPathAsync(); if (tryGetDriveRootPathResult.IsError == true) { Debug.Assert(false, "Could not get removable drive's root path"); App.Current.Logger.LogError("Could not get removable drive's root path"); // gracefully degrade; skip this disk continue; } var driveRootPath = tryGetDriveRootPathResult.Value !; // NOTE: there is also an API call which may be able to do this more directly // see: https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems // NOTE: we might also consider getting the current process for Explorer.exe and then asking it to "explore" the drive App.Current.Logger.LogError("Opening USB drive"); Process.Start(new ProcessStartInfo() { FileName = driveRootPath, UseShellExecute = true }); } return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SignOutAsync(FunctionArgs args) { var success = Morphic.WindowsNative.WindowsSession.WindowsSession.LogOff(); return(success ? MorphicResult.OkResult() : MorphicResult.ErrorResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SendKeysAsync(FunctionArgs args) { await SelectionReader.Default.ActivateLastActiveWindow(); System.Windows.Forms.SendKeys.SendWait(args["keys"]); return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ScreenSnipAsync(FunctionArgs args) { // Hide all application windows Dictionary <Window, double> opacity = new Dictionary <Window, double>(); HashSet <Window> visible = new HashSet <Window>(); try { foreach (Window window in App.Current.Windows) { if (window is BarWindow || window is QuickHelpWindow) { if (window.AllowsTransparency) { opacity[window] = window.Opacity; window.Opacity = 0; } else { visible.Add(window); window.Visibility = Visibility.Collapsed; } } } // Give enough time for the windows to disappear await Task.Delay(500); //// method 1: hold down the windows key while pressing shift + s //// NOTE: this method does not seem to work when we have uiAccess set to true in our manifest (oddly) //const uint windowsKey = 0x5b; // VK_LWIN //Keyboard.PressKey(windowsKey, true); //System.Windows.Forms.SendKeys.SendWait("+s"); //Keyboard.PressKey(windowsKey, false); // method 2: open up the special windows URI of ms-screenclip: var openPath = "ms-screenclip:"; Process.Start(new ProcessStartInfo(openPath) { UseShellExecute = true }); } finally { // Give enough time for snip tool to grab the screen without the morphic UI. await Task.Delay(3000); // Restore the windows foreach ((Window window, double o) in opacity) { window.Opacity = o; } foreach (Window window in visible) { window.Visibility = Visibility.Visible; } } return(MorphicResult.OkResult()); }
public static async Task <bool> ReadAloud(FunctionArgs args) { string action = args["action"]; switch (action) { case "pause": App.Current.Logger.LogError("ReadAloud: pause not supported"); break; case "stop": case "play": Functions.speechPlayer?.Stop(); Functions.speechPlayer?.Dispose(); Functions.speechPlayer = null; if (action == "stop") { break; } App.Current.Logger.LogDebug("ReadAloud: Storing clipboard"); IDataObject?clipboardData = Clipboard.GetDataObject(); Dictionary <string, object?>?dataStored = null; if (clipboardData != null) { dataStored = clipboardData.GetFormats() .ToDictionary(format => format, format => (object?)clipboardData.GetData(format, false)); } Clipboard.Clear(); // Get the selection App.Current.Logger.LogDebug("ReadAloud: Getting selected text"); await SelectionReader.Default.GetSelectedText(System.Windows.Forms.SendKeys.SendWait); string text = Clipboard.GetText(); // Restore the clipboard App.Current.Logger.LogDebug("ReadAloud: Restoring clipboard"); Clipboard.Clear(); dataStored?.Where(kv => kv.Value != null).ToList() .ForEach(kv => Clipboard.SetData(kv.Key, kv.Value)); // Talk the talk SpeechSynthesizer synth = new SpeechSynthesizer(); SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text); speechPlayer = new SoundPlayer(stream.AsStream()); speechPlayer.LoadCompleted += (o, args) => { speechPlayer.Play(); }; speechPlayer.LoadAsync(); break; } return(true); }
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SetVolumeAsync(FunctionArgs args) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { // NOTE: ideally we should switch this functionality to use AudioEndpoint.SetMasterVolumeLevel instead // [it may not be practical to do that, however, if AudioEndpoint.SetMasterVolumeLevel doesn't activate to on-screen volume change dialog in Windows 10/11; to be tested...] var percent = Convert.ToUInt32(args["amount"]); // clean up the percent argument (if it's not even, is out of range, etc.) if (percent % 2 != 0) { percent += 1; } percent = System.Math.Clamp(percent, 0, 100); if (args["direction"] == "up") { var sendCommandResult = Morphic.WindowsNative.Audio.Utils.VolumeUtils.SendVolumeUpCommand(percent); if (sendCommandResult.IsError == true) { return(MorphicResult.ErrorResult()); } } else { var sendCommandResult = Morphic.WindowsNative.Audio.Utils.VolumeUtils.SendVolumeDownCommand(percent); if (sendCommandResult.IsError == true) { return(MorphicResult.ErrorResult()); } } return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > VolumeMuteAsync(FunctionArgs args) { bool newState; if (args.Arguments.Keys.Contains("state")) { newState = (args["state"] == "on"); } else { var getMuteStateResult = Functions.GetMuteState(); if (getMuteStateResult.IsSuccess == true) { newState = getMuteStateResult.Value !; } else { // if we cannot get the current value, gracefully degrade (i.e. assume that the volume is not muted) newState = false; } } // set the mute state to the new state value var getDefaultAudioOutputEndpointResult = Morphic.WindowsNative.Audio.AudioEndpoint.GetDefaultAudioOutputEndpoint(); if (getDefaultAudioOutputEndpointResult.IsError == true) { return(MorphicResult.ErrorResult()); } var audioEndpoint = getDefaultAudioOutputEndpointResult.Value !; var setMasterMuteStateResult = audioEndpoint.SetMasterMuteState(newState); if (setMasterMuteStateResult.IsError == true) { return(MorphicResult.ErrorResult()); } return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > VolumeDownAsync(FunctionArgs args) { args.Arguments.Add("direction", "down"); args.Arguments.Add("amount", "6"); return(await SetVolumeAsync(args)); }
public static Task <bool> ShowMenu(FunctionArgs args) { App.Current.ShowMenu(); return(Task.FromResult(true)); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > DarkModeAsync(FunctionArgs args) { // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state" bool on; if (args.Arguments.Keys.Contains("value")) { on = (args["value"] == "on"); } else if (args.Arguments.Keys.Contains("state")) { on = (args["state"] == "on"); } else { System.Diagnostics.Debug.Assert(false, "Function 'darkMode' did not receive a new state"); on = false; } var setDarkModeStateResult = await Functions.SetDarkModeStateAsync(on); if (setDarkModeStateResult.IsError == true) { return(MorphicResult.ErrorResult()); } return(MorphicResult.OkResult()); }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ToggleBasicWordRibbonAsync(FunctionArgs args) { // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state" bool on; if (args.Arguments.Keys.Contains("value")) { on = (args["value"] == "on"); } else if (args.Arguments.Keys.Contains("state")) { on = (args["state"] == "on"); } else { System.Diagnostics.Debug.Assert(false, "Function 'basicWordRibbon' did not receive a new state"); on = false; } if (Functions.IsSafeToModifyRibbonFile_WarnUser() == false) { // Word is running, so we are choosing not to execute this function return(MorphicResult.ErrorResult()); } if (on == true) { var enableRibbonResult = Morphic.Integrations.Office.WordRibbon.EnableBasicSimplifyRibbon(); return(enableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult()); } else { var disableRibbonResult = Morphic.Integrations.Office.WordRibbon.DisableBasicSimplifyRibbon(); return(disableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult()); } }
public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ReadAloudAsync(FunctionArgs args) { string action = args["action"]; switch (action) { case "pause": App.Current.Logger.LogError("ReadAloud: pause not supported."); return(MorphicResult.ErrorResult()); case "stop": App.Current.Logger.LogDebug("ReadAloud: Stop reading selected text."); TextToSpeechHelper.Instance.Stop(); return(MorphicResult.OkResult()); case "play": string?selectedText = null; try { App.Current.Logger.LogDebug("ReadAloud: Getting selected text."); // activate the target window (i.e. topmost/last-active window, rather than the MorphicBar); we will then capture the current selection in that window // NOTE: ideally we would activate the last window as part of our atomic operation, but we really have no control over whether or not another application // or the user changes the activated window (and our internal code is also not set up to block us from moving activation/focus temporarily). await SelectionReader.Default.ActivateLastActiveWindow(); // as a primary strategy, try using the built-in Windows functionality for capturing the current selection via UI automation // NOTE: this does not work with some apps (such as Internet Explorer...but also others) bool captureTextViaAutomationSucceeded = false; // TextPatternRange[]? textRangeCollection = null; // // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block await s_captureTextSemaphore.WaitAsync(); // try { var focusedElement = AutomationElement.FocusedElement; if (focusedElement is not null) { object?pattern = null; if (focusedElement.TryGetCurrentPattern(TextPattern.Pattern, out pattern)) { if ((pattern is not null) && (pattern is TextPattern textPattern)) { // App.Current.Logger.LogDebug("ReadAloud: Capturing select text range(s)."); // get the collection of text ranges in the selection; note that this can be a disjoint collection if multiple disjoint items were selected textRangeCollection = textPattern.GetSelection(); } } else { App.Current.Logger.LogDebug("ReadAloud: Selected element is not text."); } } else { App.Current.Logger.LogDebug("ReadAloud: No element is currently selected."); } } finally { s_captureTextSemaphore.Release(); } // // if we just captured a text range collection (i.e. were able to copy the current selection), convert that capture into a string now StringBuilder?selectedTextBuilder = null; if (textRangeCollection is not null) { // we have captured a range (presumably either an empty or non-empty selection) selectedTextBuilder = new StringBuilder(); // append each text range foreach (var textRange in textRangeCollection) { if (textRange is not null) { selectedTextBuilder.Append(textRange.GetText(-1 /* maximumRange */)); } } //if (selectedTextBuilder is not null /* && stringBuilder.Length > 0 */) //{ selectedText = selectedTextBuilder.ToString(); captureTextViaAutomationSucceeded = true; if (selectedText != String.Empty) { App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); } else { App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); } //} } // as a backup strategy, use the clipboard and send ctrl+c to the target window to capture the text contents (while preserving as much of the previous // clipboard's contents as possible); this is necessary in Internet Explorer and some other programs if (captureTextViaAutomationSucceeded == false) { // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block await s_captureTextSemaphore.WaitAsync(); // try { // App.Current.Logger.LogDebug("ReadAloud: Attempting to back up current clipboard."); Dictionary <String, object?> clipboardContentsToRestore = new Dictionary <string, object?>(); var previousClipboardData = Clipboard.GetDataObject(); if (previousClipboardData is not null) { // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to capture format(s) of contents."); string[]? previousClipboardFormats = previousClipboardData.GetFormats(); if (previousClipboardFormats is not null) { // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to back up current clipboard."); foreach (var format in previousClipboardFormats) { object?dataObject; try { dataObject = previousClipboardData.GetData(format, false /* autoConvert */); } catch { // NOTE: in the future, we should look at using Project Reunion to use the UWP APIs (if they can deal with this scenario better) // see: https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.clipboard?view=winrt-19041 // see: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance App.Current.Logger.LogDebug("ReadAloud: Unable to back up clipboard contents; this can happen with files copied to the clipboard, etc."); return(MorphicResult.ErrorResult()); } clipboardContentsToRestore[format] = dataObject; } } else { App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents, but we were unable to obtain their formats."); } } else { App.Current.Logger.LogDebug("ReadAloud: Current clipboard has no contents."); } // clear the current clipboard App.Current.Logger.LogDebug("ReadAloud: Clearing the current clipboard."); try { // try to clear the clipboard for up to 500ms (4 delays of 125ms) await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125)); } catch { App.Current.Logger.LogDebug("ReadAloud: Could not clear the current clipboard."); } // copy the current selection to the clipboard App.Current.Logger.LogDebug("ReadAloud: Sending Ctrl+C to copy the current selection to the clipboard."); await SelectionReader.Default.GetSelectedTextAsync(System.Windows.Forms.SendKeys.SendWait); // wait 100ms (an arbitrary amount of time, but in our testing some wait is necessary...even with the WM-triggered copy logic above) // NOTE: perhaps, in the future, we should only do this if our first call to Clipboard.GetText() returns (null? or) an empty string; // or perhaps we should wait up to a certain number of milliseconds to receive a SECOND WM (the one that GetSelectedTextAsync // waited for). await Task.Delay(100); // capture the current selection var selectionWasCopiedToClipboard = false; var textCopiedToClipboard = Clipboard.GetText(); if (textCopiedToClipboard is not null) { selectionWasCopiedToClipboard = true; // we now have our selected text selectedText = textCopiedToClipboard; if (selectedText is not null) { App.Current.Logger.LogDebug("ReadAloud: Captured selected text."); } else { App.Current.Logger.LogDebug("ReadAloud: Captured empty selection."); } } else { var copiedDataFormats = Clipboard.GetDataObject()?.GetFormats(); if (copiedDataFormats is not null) { selectionWasCopiedToClipboard = true; // var formatsCsvBuilder = new StringBuilder(); // formatsCsvBuilder.Append("["); // if (copiedDataFormats.Length > 0) // { // formatsCsvBuilder.Append("\""); // formatsCsvBuilder.Append(String.Join("\", \"", copiedDataFormats)); // formatsCsvBuilder.Append("\""); // } // formatsCsvBuilder.Append("]"); // App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy text; instead it copied data in these format(s): " + formatsCsvBuilder.ToString()); App.Current.Logger.LogDebug("ReadAloud: Ctrl+C copied non-text (un-speakable) contents to the clipboard."); } else { App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy anything to the clipboard."); } } // restore the previous clipboard's contents // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore the previous clipboard's contents"); // if (selectionWasCopiedToClipboard == true) { // App.Current.Logger.LogDebug("ReadAloud: Clearing the selected text from the clipboard."); try { // try to clear the clipboard for up to 500ms (4 delays of 125ms) await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125)); } catch { App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard."); } } // if (clipboardContentsToRestore.Count > 0) { // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore " + clipboardContentsToRestore.Count.ToString() + " item(s) to the clipboard."); } else { // App.Current.Logger.LogDebug("ReadAloud: there is nothing to restore to the clipboard."); } // foreach (var(format, data) in clipboardContentsToRestore) { // NOTE: sometimes, data is null (which is not something that SetData can accept) so we have to just skip that element if (data is not null) { Clipboard.SetData(format, data); } } // App.Current.Logger.LogDebug("ReadAloud: Clipboard restoration complete"); } finally { s_captureTextSemaphore.Release(); } } } catch (Exception ex) { App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text."); return(MorphicResult.ErrorResult()); } if (selectedText is not null) { if (selectedText != String.Empty) { try { App.Current.Logger.LogDebug("ReadAloud: Saying selected text."); var sayResult = await TextToSpeechHelper.Instance.Say(selectedText); if (sayResult.IsError == true) { App.Current.Logger.LogError("ReadAloud: Error saying selected text."); return(MorphicResult.ErrorResult()); } return(MorphicResult.OkResult()); } catch (Exception ex) { App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text."); return(MorphicResult.ErrorResult()); } } else { App.Current.Logger.LogDebug("ReadAloud: No text to say; skipping 'say' command."); return(MorphicResult.OkResult()); } } else { // could not capture any text // App.Current.Logger.LogError("ReadAloud: Could not capture any selected text; this may or may not be an error."); return(MorphicResult.ErrorResult()); } default: throw new Exception("invalid code path"); } }