Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion Tests/PostGrabActionManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void GetDefaultPostGrabActions_ReturnsExpectedCount()

// Assert
Assert.NotNull(actions);
Assert.Equal(5, actions.Count);
Assert.Equal(6, actions.Count);
}

[Fact]
Expand All @@ -28,6 +28,7 @@ public void GetDefaultPostGrabActions_ContainsExpectedActions()
Assert.Contains(actions, a => a.ButtonText == "Remove duplicate lines");
Assert.Contains(actions, a => a.ButtonText == "Web Search");
Assert.Contains(actions, a => a.ButtonText == "Try to insert text");
Assert.Contains(actions, a => a.ButtonText == "Speak text");
//Assert.Contains(actions, a => a.ButtonText == "Translate to system language");
}

Expand Down Expand Up @@ -103,6 +104,21 @@ public async System.Threading.Tasks.Task ExecutePostGrabAction_RemoveDuplicateLi
Assert.Single(lines, l => l == "Line 1");
}

[Fact]
public async Task ExecutePostGrabAction_SpeakText_ReturnsTextUnchanged()
{
// Arrange
ButtonInfo action = PostGrabActionManager.GetDefaultPostGrabActions()
.First(a => a.ClickEvent == "SpeakText_Click");
string input = "Hello world";

// Act
string result = await PostGrabActionManager.ExecutePostGrabAction(action, input);

// Assert — TTS is fire-and-forget; text must pass through unchanged
Assert.Equal(input, result);
}

[Fact]
public void GetCheckState_DefaultOff_ReturnsFalse()
{
Expand Down
3 changes: 3 additions & 0 deletions Text-Grab/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
<setting name="RunInTheBackground" serializeAs="String">
<value>False</value>
</setting>
<setting name="TtsVoiceName" serializeAs="String">
<value />
</setting>
<setting name="TryInsert" serializeAs="String">
<value>False</value>
</setting>
Expand Down
6 changes: 5 additions & 1 deletion Text-Grab/Controls/WordBorder.xaml.cs
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for this implementation but makes me think there needs to be a more central service for handling 'text action' like copy/speak/etc.

We don't need to solve now just a thought

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Windows.Media;
using System.Windows.Threading;
using Text_Grab.Models;
using Text_Grab.Services;
using Text_Grab.Utilities;
using Text_Grab.Views;

Expand Down Expand Up @@ -449,7 +450,10 @@ private void WordBorderControl_MouseDoubleClick(object sender, MouseButtonEventA

try { Clipboard.SetDataObject(Word, true); } catch { }

if (AppUtilities.TextGrabSettings.ShowToast
if (AppUtilities.TextGrabSettings.SpeakInsteadOfToast
&& !IsFromEditWindow)
Singleton<TtsService>.Instance.Speak(Word);
else if (AppUtilities.TextGrabSettings.ShowToast
&& !IsFromEditWindow)
NotificationUtilities.ShowToast(Word);

Expand Down
9 changes: 9 additions & 0 deletions Text-Grab/Interfaces/ITtsEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;

namespace Text_Grab.Interfaces;

public interface ITtsEngine
{
Task SpeakAsync(string text, CancellationToken ct);
}
2 changes: 1 addition & 1 deletion Text-Grab/Pages/GeneralSettings.xaml
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a radio group with the notifications to be

After grab always:

  • Nothing
  • Show notification
  • Speak text aloud

Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@
<TextBlock Margin="0,4,0,0" Style="{StaticResource TextBodyNormal}">
Clicking the notification opens the copied text into a new Edit Text Window to display and edit text.
</TextBlock>

<!-- default launch -->
<TextBlock
Margin="0,16,0,4"
Expand Down Expand Up @@ -387,5 +386,6 @@
Keep recent history of Grabs and Edit Text Windows
</TextBlock>
</ui:ToggleSwitch>

</StackPanel>
</Page>
1 change: 0 additions & 1 deletion Text-Grab/Pages/GeneralSettings.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ private async void Page_Loaded(object sender, RoutedEventArgs e)
TryInsertCheckbox.IsChecked = DefaultSettings.TryInsert;
InsertDelaySeconds = DefaultSettings.InsertDelay;
SecondsTextBox.Text = InsertDelaySeconds.ToString("##.#", System.Globalization.CultureInfo.InvariantCulture);

// Context menu integration - only available for unpackaged apps
if (!AppUtilities.IsPackaged())
{
Expand Down
100 changes: 100 additions & 0 deletions Text-Grab/Pages/VoiceOutputSettings.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<Page
x:Class="Text_Grab.Pages.VoiceOutputSettings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="VoiceOutputSettings"
d:DesignHeight="700"
d:DesignWidth="800"
Loaded="Page_Loaded"
mc:Ignorable="d">

<ScrollViewer VerticalScrollBarVisibility="Visible">
<StackPanel Margin="20,12,40,40" Orientation="Vertical">
<TextBlock Style="{StaticResource TextHeader}" Text="Voice Output Settings" />

<!-- Speak instead of notification -->
<TextBlock
Margin="0,16,0,4"
FontSize="16"
Style="{StaticResource TextHeader}"
Text="Notification Behaviour" />
<ui:ToggleSwitch
x:Name="SpeakInsteadOfToastToggle"
Checked="SpeakInsteadOfToastToggle_Checked"
Unchecked="SpeakInsteadOfToastToggle_Checked">
<TextBlock Style="{StaticResource TextBodyNormal}">
Speak text instead of showing notification
</TextBlock>
</ui:ToggleSwitch>
<TextBlock Margin="0,4,0,0" Style="{StaticResource TextBodyNormal}">
Speaks the grabbed text aloud rather than showing a notification.
</TextBlock>
Comment on lines +24 to +34
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this is enabled AND the Post grab 'speak' option is enabled the text is spoken twice.


<!-- Voice selection -->
<TextBlock
Margin="0,16,0,4"
FontSize="16"
Style="{StaticResource TextHeader}"
Text="Voice" />
<TextBlock Margin="0,0,0,6" Style="{StaticResource TextBodyNormal}">
Choose from the voices installed on this device.
</TextBlock>
<ComboBox
x:Name="VoiceComboBox"
Width="320"
HorizontalAlignment="Left"
SelectionChanged="VoiceComboBox_SelectionChanged" />

<!-- Word limit -->
<TextBlock
Margin="0,16,0,4"
FontSize="16"
Style="{StaticResource TextHeader}"
Text="Word Limit" />
<TextBlock Margin="0,0,0,6" Style="{StaticResource TextBodyNormal}">
Stop speaking after this many words. Set to 0 for no limit.
</TextBlock>
<StackPanel Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource TextBodyNormal}"
Text="Speak word limit:" />
<TextBox
x:Name="TtsSpeakWordLimitTextBox"
Width="60"
Height="26"
Margin="8,0,0,0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="White"
FontWeight="Medium"
Foreground="Black"
Style="{StaticResource TextBoxStyle1}"
TextChanged="TtsSpeakWordLimitTextBox_TextChanged" />
<TextBlock
x:Name="TtsWordLimitError"
Margin="12,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource TextBodyNormal}"
Text="⚠ Must be a positive number"
Visibility="Collapsed" />
</StackPanel>

<!-- Preview -->
<TextBlock
Margin="0,16,0,4"
FontSize="16"
Style="{StaticResource TextHeader}"
Text="Preview" />
<Button
x:Name="PreviewVoiceButton"
HorizontalAlignment="Left"
Click="PreviewVoiceButton_Click"
Content="Speak sample" />

</StackPanel>
</ScrollViewer>
</Page>
82 changes: 82 additions & 0 deletions Text-Grab/Pages/VoiceOutputSettings.xaml.cs
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems good for an MVP of this feature. The only thing I think might be nice is adding a way to set synthesizer.Options.SpeakingRate since the default setting felt a little slow. Also if a user has all of these speech options set up on their Windows settings do those flow through here or do they need to be set twice?

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using Text_Grab.Properties;
using Text_Grab.Services;
using Text_Grab.Utilities;
using Windows.Media.SpeechSynthesis;

namespace Text_Grab.Pages;

public partial class VoiceOutputSettings : Page
{
private readonly Settings DefaultSettings = AppUtilities.TextGrabSettings;
private bool _loaded = false;

public VoiceOutputSettings()
{
InitializeComponent();
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
SpeakInsteadOfToastToggle.IsChecked = DefaultSettings.SpeakInsteadOfToast;

VoiceComboBox.Items.Clear();
foreach (VoiceInformation voice in SpeechSynthesizer.AllVoices.OrderBy(v => v.DisplayName))
VoiceComboBox.Items.Add(voice.DisplayName);

string savedVoice = DefaultSettings.TtsVoiceName;
if (!string.IsNullOrEmpty(savedVoice) && VoiceComboBox.Items.Contains(savedVoice))
VoiceComboBox.SelectedItem = savedVoice;
else
VoiceComboBox.SelectedIndex = 0;

TtsSpeakWordLimitTextBox.Text = DefaultSettings.TtsSpeakWordLimit.ToString();

_loaded = true;
}

private void SpeakInsteadOfToastToggle_Checked(object sender, RoutedEventArgs e)
{
if (!_loaded)
return;

DefaultSettings.SpeakInsteadOfToast = SpeakInsteadOfToastToggle.IsChecked is true;
DefaultSettings.Save();
}

private void VoiceComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!_loaded)
return;

if (VoiceComboBox.SelectedItem is string voiceName)
{
DefaultSettings.TtsVoiceName = voiceName;
DefaultSettings.Save();
}
}

private void TtsSpeakWordLimitTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (!_loaded)
return;

if (int.TryParse(TtsSpeakWordLimitTextBox.Text, out int parsedValue) && parsedValue > 0)
{
DefaultSettings.TtsSpeakWordLimit = parsedValue;
DefaultSettings.Save();
TtsWordLimitError.Visibility = Visibility.Collapsed;
}
else
{
TtsWordLimitError.Visibility = Visibility.Visible;
}
}

private void PreviewVoiceButton_Click(object sender, RoutedEventArgs e)
{
Singleton<TtsService>.Instance.Speak("Hello, this is a preview of the selected voice.");
}
}
38 changes: 37 additions & 1 deletion Text-Grab/Properties/Settings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Text-Grab/Properties/Settings.settings
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<Setting Name="ShowToast" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="SpeakInsteadOfToast" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="DefaultLaunch" Type="System.String" Scope="User">
<Value Profile="(Default)">Fullscreen</Value>
</Setting>
Expand Down Expand Up @@ -57,7 +60,7 @@
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="FSGMakeSingleLineToggle" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
<Value Profile="(Default)">True</Value>
Comment on lines -60 to +63
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undo this change, keep the default option for Make single line as False

</Setting>
<Setting Name="GlobalHotkeysEnabled" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
Expand Down Expand Up @@ -236,5 +239,11 @@
<Setting Name="RegisterOpenWith" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="TtsSpeakWordLimit" Type="System.Int32" Scope="User">
<Value Profile="(Default)">100</Value>
</Setting>
<Setting Name="TtsVoiceName" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>
Loading