2951 lines
90 KiB
C++
2951 lines
90 KiB
C++
#include "HyperTwistUX/HyperTwistSettingsPanelWidget.h"
|
|
|
|
#include "Blueprint/WidgetTree.h"
|
|
#include "Components/Border.h"
|
|
#include "Components/Button.h"
|
|
#include "Components/ButtonSlot.h"
|
|
#include "Components/CheckBox.h"
|
|
#include "Components/ComboBoxString.h"
|
|
#include "Components/EditableTextBox.h"
|
|
#include "Components/HorizontalBox.h"
|
|
#include "Components/HorizontalBoxSlot.h"
|
|
#include "Components/ScrollBox.h"
|
|
#include "Components/SizeBox.h"
|
|
#include "Components/Slider.h"
|
|
#include "Components/Spacer.h"
|
|
#include "Components/TextBlock.h"
|
|
#include "Components/UniformGridPanel.h"
|
|
#include "Components/UniformGridSlot.h"
|
|
#include "Components/VerticalBox.h"
|
|
#include "Components/VerticalBoxSlot.h"
|
|
#include "Components/WidgetSwitcher.h"
|
|
#include "Input/Reply.h"
|
|
#include "InputCoreTypes.h"
|
|
|
|
namespace HyperTwistSettingsPanelWidgetInternal
|
|
{
|
|
constexpr int32 ControlsPageIndex = 0;
|
|
constexpr int32 AudioPageIndex = 1;
|
|
constexpr int32 GraphicsPageIndex = 2;
|
|
constexpr int32 AccessibilityPageIndex = 3;
|
|
constexpr int32 SpeechPageIndex = 4;
|
|
|
|
struct FProviderOption
|
|
{
|
|
const TCHAR* Id;
|
|
const TCHAR* Label;
|
|
const TCHAR* DefaultEndpoint;
|
|
const TCHAR* DefaultModel;
|
|
};
|
|
|
|
const TArray<FProviderOption>& GetSpeechProviderOptions()
|
|
{
|
|
static const TArray<FProviderOption> Options = {
|
|
{TEXT("local-whisper-cpp"), TEXT("On-device Whisper"), TEXT("http://127.0.0.1:8766"), TEXT("local-whisper/base-q5_1")},
|
|
{TEXT("local-freestyle"), TEXT("Local Freestyle bridge"), TEXT("http://127.0.0.1:4649"), TEXT("local-whisper/base-q5_1")},
|
|
{TEXT("openai"), TEXT("OpenAI via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("openai/gpt-4o-transcribe")},
|
|
{TEXT("groq"), TEXT("Groq via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("groq/whisper-large-v3-turbo")},
|
|
{TEXT("deepgram"), TEXT("Deepgram via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("deepgram/nova-3")},
|
|
{TEXT("elevenlabs"), TEXT("ElevenLabs via speech bridge"), TEXT("http://127.0.0.1:8766"), TEXT("elevenlabs/scribe_v2_realtime")}
|
|
};
|
|
return Options;
|
|
}
|
|
|
|
const TArray<FProviderOption>& GetVoiceProviderOptions()
|
|
{
|
|
static const TArray<FProviderOption> Options = {
|
|
{TEXT("local-piper"), TEXT("On-device Piper"), TEXT("http://127.0.0.1:8766"), TEXT("piper-medium")},
|
|
{TEXT("openai"), TEXT("OpenAI via voice bridge"), TEXT("http://127.0.0.1:8766"), TEXT("gpt-4o-mini-tts")},
|
|
{TEXT("elevenlabs"), TEXT("ElevenLabs via voice bridge"), TEXT("http://127.0.0.1:8766"), TEXT("eleven_multilingual_v2")},
|
|
{TEXT("custom-compatible"), TEXT("Custom compatible service"), TEXT(""), TEXT("")}
|
|
};
|
|
return Options;
|
|
}
|
|
|
|
const TArray<FProviderOption>& GetCoachProviderOptions()
|
|
{
|
|
static const TArray<FProviderOption> Options = {
|
|
{TEXT("local-disabled"), TEXT("Built-in guide only"), TEXT(""), TEXT("")},
|
|
{TEXT("local-openai-compatible"), TEXT("Local OpenAI-compatible"), TEXT("http://127.0.0.1:11434/v1"), TEXT("")},
|
|
{TEXT("openai"), TEXT("OpenAI"), TEXT("https://api.openai.com/v1/responses"), TEXT("gpt-5.5")},
|
|
{TEXT("groq"), TEXT("Groq"), TEXT("https://api.groq.com/openai/v1"), TEXT("")},
|
|
{TEXT("custom-openai-compatible"), TEXT("Custom OpenAI-compatible"), TEXT(""), TEXT("")}
|
|
};
|
|
return Options;
|
|
}
|
|
|
|
const FProviderOption* FindProviderById(
|
|
const TArray<FProviderOption>& Options,
|
|
const FString& Id
|
|
)
|
|
{
|
|
return Options.FindByPredicate(
|
|
[&Id](const FProviderOption& Option)
|
|
{
|
|
return Id.Equals(Option.Id, ESearchCase::IgnoreCase);
|
|
});
|
|
}
|
|
|
|
const FProviderOption* FindProviderByLabel(
|
|
const TArray<FProviderOption>& Options,
|
|
const FString& Label
|
|
)
|
|
{
|
|
return Options.FindByPredicate(
|
|
[&Label](const FProviderOption& Option)
|
|
{
|
|
return Label.Equals(Option.Label, ESearchCase::IgnoreCase);
|
|
});
|
|
}
|
|
|
|
TArray<FString> BuildProviderLabels(const TArray<FProviderOption>& Options)
|
|
{
|
|
TArray<FString> Labels;
|
|
Labels.Reserve(Options.Num());
|
|
for (const FProviderOption& Option : Options)
|
|
{
|
|
Labels.Add(Option.Label);
|
|
}
|
|
return Labels;
|
|
}
|
|
|
|
FString ProviderLabel(
|
|
const TArray<FProviderOption>& Options,
|
|
const FString& Id
|
|
)
|
|
{
|
|
if (const FProviderOption* Option = FindProviderById(Options, Id))
|
|
{
|
|
return Option->Label;
|
|
}
|
|
return Options.Num() > 0 ? FString(Options[0].Label) : FString();
|
|
}
|
|
|
|
FString SpeechLanguageLabel(const FString& LanguageCode)
|
|
{
|
|
if (LanguageCode.Equals(TEXT("en"), ESearchCase::IgnoreCase)) return TEXT("English");
|
|
if (LanguageCode.Equals(TEXT("de"), ESearchCase::IgnoreCase)) return TEXT("German");
|
|
if (LanguageCode.Equals(TEXT("fr"), ESearchCase::IgnoreCase)) return TEXT("French");
|
|
if (LanguageCode.Equals(TEXT("es"), ESearchCase::IgnoreCase)) return TEXT("Spanish");
|
|
if (LanguageCode.Equals(TEXT("it"), ESearchCase::IgnoreCase)) return TEXT("Italian");
|
|
if (LanguageCode.Equals(TEXT("pt"), ESearchCase::IgnoreCase)) return TEXT("Portuguese");
|
|
if (LanguageCode.Equals(TEXT("ja"), ESearchCase::IgnoreCase)) return TEXT("Japanese");
|
|
if (LanguageCode.Equals(TEXT("ko"), ESearchCase::IgnoreCase)) return TEXT("Korean");
|
|
if (LanguageCode.Equals(TEXT("zh"), ESearchCase::IgnoreCase)) return TEXT("Chinese");
|
|
return TEXT("Auto-detect");
|
|
}
|
|
|
|
FString SpeechLanguageCode(const FString& LanguageLabel)
|
|
{
|
|
if (LanguageLabel == TEXT("English")) return TEXT("en");
|
|
if (LanguageLabel == TEXT("German")) return TEXT("de");
|
|
if (LanguageLabel == TEXT("French")) return TEXT("fr");
|
|
if (LanguageLabel == TEXT("Spanish")) return TEXT("es");
|
|
if (LanguageLabel == TEXT("Italian")) return TEXT("it");
|
|
if (LanguageLabel == TEXT("Portuguese")) return TEXT("pt");
|
|
if (LanguageLabel == TEXT("Japanese")) return TEXT("ja");
|
|
if (LanguageLabel == TEXT("Korean")) return TEXT("ko");
|
|
if (LanguageLabel == TEXT("Chinese")) return TEXT("zh");
|
|
return TEXT("auto");
|
|
}
|
|
|
|
FString PercentLabel(const float Value)
|
|
{
|
|
return FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Value * 100.0f));
|
|
}
|
|
|
|
FString DecimalLabel(const float Value)
|
|
{
|
|
return FString::Printf(TEXT("%.2fx"), Value);
|
|
}
|
|
|
|
void SetText(UTextBlock* TextBlock, const FString& Text)
|
|
{
|
|
if (TextBlock != nullptr)
|
|
{
|
|
TextBlock->SetText(FText::FromString(Text));
|
|
}
|
|
}
|
|
|
|
FString QualityLabel(const EHyperTwistGraphicsQuality Quality)
|
|
{
|
|
switch (Quality)
|
|
{
|
|
case EHyperTwistGraphicsQuality::Low:
|
|
return TEXT("Low");
|
|
case EHyperTwistGraphicsQuality::Medium:
|
|
return TEXT("Medium");
|
|
case EHyperTwistGraphicsQuality::Epic:
|
|
return TEXT("Epic");
|
|
default:
|
|
return TEXT("High");
|
|
}
|
|
}
|
|
|
|
FString WindowModeLabel(const EHyperTwistWindowMode WindowMode)
|
|
{
|
|
switch (WindowMode)
|
|
{
|
|
case EHyperTwistWindowMode::Fullscreen:
|
|
return TEXT("Fullscreen");
|
|
case EHyperTwistWindowMode::Windowed:
|
|
return TEXT("Windowed");
|
|
default:
|
|
return TEXT("Borderless");
|
|
}
|
|
}
|
|
}
|
|
|
|
void UHyperTwistKeyBindingRowWidget::ConfigureBinding(
|
|
const FName InActionId,
|
|
const FString& InLabel,
|
|
const FString& InCategory,
|
|
const FKey InKey
|
|
)
|
|
{
|
|
ActionId = InActionId;
|
|
DisplayLabel = InLabel;
|
|
CategoryLabel = InCategory;
|
|
BoundKey = InKey;
|
|
EnsureWidgetTreeBuilt();
|
|
if (BindingLabel != nullptr)
|
|
{
|
|
BindingLabel->SetText(FText::FromString(DisplayLabel));
|
|
}
|
|
UpdateBoundKey(BoundKey);
|
|
}
|
|
|
|
void UHyperTwistKeyBindingRowWidget::UpdateBoundKey(
|
|
const FKey InKey,
|
|
const bool bAwaitingInput
|
|
)
|
|
{
|
|
BoundKey = InKey;
|
|
if (KeyLabel != nullptr)
|
|
{
|
|
KeyLabel->SetText(FText::FromString(
|
|
bAwaitingInput ? TEXT("Press a key...") : BoundKey.GetDisplayName().ToString()));
|
|
}
|
|
}
|
|
|
|
TSharedRef<SWidget> UHyperTwistKeyBindingRowWidget::RebuildWidget()
|
|
{
|
|
Initialize();
|
|
EnsureWidgetTreeBuilt();
|
|
return Super::RebuildWidget();
|
|
}
|
|
|
|
void UHyperTwistKeyBindingRowWidget::EnsureWidgetTreeBuilt()
|
|
{
|
|
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UHorizontalBox* Row = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("KeyBindingRow"));
|
|
WidgetTree->RootWidget = Row;
|
|
|
|
USizeBox* LabelSize = WidgetTree->ConstructWidget<USizeBox>(
|
|
USizeBox::StaticClass(),
|
|
TEXT("KeyBindingLabelSize"));
|
|
LabelSize->SetWidthOverride(360.0f);
|
|
BindingLabel = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass(),
|
|
TEXT("KeyBindingLabel"));
|
|
BindingLabel->SetText(FText::FromString(DisplayLabel));
|
|
BindingLabel->SetColorAndOpacity(FSlateColor(FLinearColor(0.88f, 0.94f, 0.98f, 1.0f)));
|
|
FSlateFontInfo BindingFont = BindingLabel->GetFont();
|
|
BindingFont.Size = 14;
|
|
BindingLabel->SetFont(BindingFont);
|
|
LabelSize->AddChild(BindingLabel);
|
|
if (UHorizontalBoxSlot* LabelSlot = Row->AddChildToHorizontalBox(LabelSize))
|
|
{
|
|
LabelSlot->SetPadding(FMargin(0.0f, 6.0f, 12.0f, 6.0f));
|
|
LabelSlot->SetVerticalAlignment(VAlign_Center);
|
|
}
|
|
|
|
UButton* RebindButton = WidgetTree->ConstructWidget<UButton>(
|
|
UButton::StaticClass(),
|
|
TEXT("KeyBindingButton"));
|
|
RebindButton->SetBackgroundColor(FLinearColor(0.07f, 0.15f, 0.20f, 1.0f));
|
|
KeyLabel = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass(),
|
|
TEXT("KeyBindingValue"));
|
|
KeyLabel->SetText(FText::FromString(BoundKey.GetDisplayName().ToString()));
|
|
KeyLabel->SetJustification(ETextJustify::Center);
|
|
KeyLabel->SetColorAndOpacity(FSlateColor(FLinearColor(0.12f, 0.95f, 0.82f, 1.0f)));
|
|
FSlateFontInfo KeyFont = KeyLabel->GetFont();
|
|
KeyFont.Size = 14;
|
|
KeyLabel->SetFont(KeyFont);
|
|
RebindButton->AddChild(KeyLabel);
|
|
if (UButtonSlot* KeyButtonSlot = Cast<UButtonSlot>(KeyLabel->Slot))
|
|
{
|
|
KeyButtonSlot->SetPadding(FMargin(18.0f, 7.0f));
|
|
KeyButtonSlot->SetHorizontalAlignment(HAlign_Center);
|
|
}
|
|
RebindButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistKeyBindingRowWidget::HandleRebindClicked);
|
|
if (UHorizontalBoxSlot* ButtonSlot = Row->AddChildToHorizontalBox(RebindButton))
|
|
{
|
|
ButtonSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
ButtonSlot->SetPadding(FMargin(0.0f, 4.0f));
|
|
ButtonSlot->SetVerticalAlignment(VAlign_Center);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistKeyBindingRowWidget::HandleRebindClicked()
|
|
{
|
|
UpdateBoundKey(BoundKey, true);
|
|
OnRebindRequested.Broadcast(ActionId);
|
|
}
|
|
|
|
UHyperTwistSettingsPanelWidget::UHyperTwistSettingsPanelWidget(
|
|
const FObjectInitializer& ObjectInitializer
|
|
)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
SetIsFocusable(true);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::NativeConstruct()
|
|
{
|
|
Super::NativeConstruct();
|
|
EnsureWidgetTreeBuilt();
|
|
ReloadPreferences();
|
|
}
|
|
|
|
TSharedRef<SWidget> UHyperTwistSettingsPanelWidget::RebuildWidget()
|
|
{
|
|
Initialize();
|
|
EnsureWidgetTreeBuilt();
|
|
return Super::RebuildWidget();
|
|
}
|
|
|
|
bool UHyperTwistSettingsPanelWidget::PrepareSettingsSurface()
|
|
{
|
|
EnsureWidgetTreeBuilt();
|
|
ReloadPreferences();
|
|
return IsSettingsSurfaceReady();
|
|
}
|
|
|
|
bool UHyperTwistSettingsPanelWidget::IsSettingsSurfaceReady() const
|
|
{
|
|
return WidgetTree != nullptr
|
|
&& WidgetTree->RootWidget != nullptr
|
|
&& SettingsPageSwitcher != nullptr
|
|
&& SettingsPageSwitcher->GetNumWidgets() == 5
|
|
&& KeyboardProfileCombo != nullptr
|
|
&& MasterVolumeSlider != nullptr;
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ReloadPreferences()
|
|
{
|
|
Preferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
|
|
RefreshWidgetsFromPreferences();
|
|
SetStatus(TEXT("Settings are saved automatically on this device."));
|
|
}
|
|
|
|
FReply UHyperTwistSettingsPanelWidget::NativeOnKeyDown(
|
|
const FGeometry& InGeometry,
|
|
const FKeyEvent& InKeyEvent
|
|
)
|
|
{
|
|
static_cast<void>(InGeometry);
|
|
const FKey PressedKey = InKeyEvent.GetKey();
|
|
if (TryCompletePendingRebind(PressedKey))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
if (PressedKey == EKeys::Escape && bShowCloseButton)
|
|
{
|
|
OnCloseRequested.Broadcast();
|
|
return FReply::Handled();
|
|
}
|
|
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
|
|
}
|
|
|
|
FReply UHyperTwistSettingsPanelWidget::NativeOnPreviewMouseButtonDown(
|
|
const FGeometry& InGeometry,
|
|
const FPointerEvent& InMouseEvent
|
|
)
|
|
{
|
|
if (TryCompletePendingRebind(InMouseEvent.GetEffectingButton()))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
return Super::NativeOnPreviewMouseButtonDown(InGeometry, InMouseEvent);
|
|
}
|
|
|
|
bool UHyperTwistSettingsPanelWidget::TryCompletePendingRebind(
|
|
const FKey PressedKey
|
|
)
|
|
{
|
|
if (PendingRebindActionId.IsNone())
|
|
{
|
|
return false;
|
|
}
|
|
if (PressedKey == EKeys::Escape)
|
|
{
|
|
PendingRebindActionId = NAME_None;
|
|
RefreshBindingRows();
|
|
SetStatus(TEXT("Key change cancelled."));
|
|
return true;
|
|
}
|
|
|
|
FString FailureReason;
|
|
if (!UHyperTwistPlayerSettingsLibrary::TrySetKeyBinding(
|
|
Preferences,
|
|
PendingRebindActionId,
|
|
PressedKey,
|
|
FailureReason))
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
return true;
|
|
}
|
|
|
|
PendingRebindActionId = NAME_None;
|
|
RefreshBindingRows();
|
|
SavePreferencesAndApply(TEXT("Control binding updated."));
|
|
if (KeyboardProfileCombo != nullptr)
|
|
{
|
|
KeyboardProfileCombo->SetSelectedOption(TEXT("Custom"));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::EnsureWidgetTreeBuilt()
|
|
{
|
|
if (WidgetTree == nullptr || WidgetTree->RootWidget != nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Preferences = UHyperTwistPlayerSettingsLibrary::LoadPreferences();
|
|
|
|
UBorder* RootBorder = WidgetTree->ConstructWidget<UBorder>(
|
|
UBorder::StaticClass(),
|
|
TEXT("HyperTwistSettingsRoot"));
|
|
RootBorder->SetPadding(FMargin(28.0f));
|
|
RootBorder->SetBrushColor(PanelColor);
|
|
WidgetTree->RootWidget = RootBorder;
|
|
|
|
UVerticalBox* Root = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass(),
|
|
TEXT("HyperTwistSettingsLayout"));
|
|
RootBorder->AddChild(Root);
|
|
|
|
UHorizontalBox* Header = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("HyperTwistSettingsHeader"));
|
|
if (UVerticalBoxSlot* HeaderSlot = Root->AddChildToVerticalBox(Header))
|
|
{
|
|
HeaderSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 18.0f));
|
|
}
|
|
|
|
UVerticalBox* HeaderCopy = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass(),
|
|
TEXT("HyperTwistSettingsHeaderCopy"));
|
|
if (UHorizontalBoxSlot* CopySlot = Header->AddChildToHorizontalBox(HeaderCopy))
|
|
{
|
|
CopySlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
}
|
|
AddText(HeaderCopy, TEXT("SETTINGS"), 13, AccentColor);
|
|
AddText(HeaderCopy, TEXT("Shape HyperTwist around you"), 29, PrimaryTextColor);
|
|
AddText(
|
|
HeaderCopy,
|
|
TEXT("Every desktop preference is local-first, reversible, and available without a headset."),
|
|
14,
|
|
MutedTextColor);
|
|
|
|
if (bShowCloseButton)
|
|
{
|
|
UButton* CloseButton = AddActionButton(
|
|
Header,
|
|
TEXT("Close"),
|
|
TEXT("HyperTwistSettingsCloseButton"));
|
|
CloseButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCloseClicked);
|
|
}
|
|
|
|
UUniformGridPanel* Tabs = WidgetTree->ConstructWidget<UUniformGridPanel>(
|
|
UUniformGridPanel::StaticClass(),
|
|
TEXT("HyperTwistSettingsTabs"));
|
|
Tabs->SetMinDesiredSlotWidth(150.0f);
|
|
Tabs->SetSlotPadding(FMargin(3.0f));
|
|
if (UVerticalBoxSlot* TabSlot = Root->AddChildToVerticalBox(Tabs))
|
|
{
|
|
TabSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, 16.0f));
|
|
}
|
|
|
|
UButton* ControlsTab = AddTabButton(Tabs, TEXT("Controls"), 0, TEXT("SettingsControlsTab"));
|
|
UButton* AudioTab = AddTabButton(Tabs, TEXT("Audio"), 1, TEXT("SettingsAudioTab"));
|
|
UButton* GraphicsTab = AddTabButton(Tabs, TEXT("Graphics"), 2, TEXT("SettingsGraphicsTab"));
|
|
UButton* AccessibilityTab = AddTabButton(
|
|
Tabs,
|
|
TEXT("Accessibility"),
|
|
3,
|
|
TEXT("SettingsAccessibilityTab"));
|
|
UButton* SpeechTab = AddTabButton(
|
|
Tabs,
|
|
TEXT("Speech & Coach"),
|
|
4,
|
|
TEXT("SettingsSpeechTab"));
|
|
ControlsTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowControlsPage);
|
|
AudioTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowAudioPage);
|
|
GraphicsTab->OnClicked.AddDynamic(this, &UHyperTwistSettingsPanelWidget::ShowGraphicsPage);
|
|
AccessibilityTab->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::ShowAccessibilityPage);
|
|
SpeechTab->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::ShowSpeechAndCoachPage);
|
|
|
|
SettingsPageSwitcher = WidgetTree->ConstructWidget<UWidgetSwitcher>(
|
|
UWidgetSwitcher::StaticClass(),
|
|
TEXT("HyperTwistSettingsPageSwitcher"));
|
|
if (UVerticalBoxSlot* SwitcherSlot = Root->AddChildToVerticalBox(SettingsPageSwitcher))
|
|
{
|
|
SwitcherSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
}
|
|
|
|
BuildControlsPage(AddScrollablePage(TEXT("SettingsControlsPage")));
|
|
BuildAudioPage(AddScrollablePage(TEXT("SettingsAudioPage")));
|
|
BuildGraphicsPage(AddScrollablePage(TEXT("SettingsGraphicsPage")));
|
|
BuildAccessibilityPage(AddScrollablePage(TEXT("SettingsAccessibilityPage")));
|
|
BuildSpeechAndCoachPage(AddScrollablePage(TEXT("SettingsSpeechPage")));
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::ControlsPageIndex);
|
|
|
|
StatusText = AddText(
|
|
Root,
|
|
TEXT("Settings are saved automatically on this device."),
|
|
13,
|
|
MutedTextColor,
|
|
TEXT("HyperTwistSettingsStatus"));
|
|
if (UVerticalBoxSlot* StatusSlot = Cast<UVerticalBoxSlot>(
|
|
StatusText != nullptr ? StatusText->Slot : nullptr))
|
|
{
|
|
StatusSlot->SetPadding(FMargin(0.0f, 14.0f, 0.0f, 0.0f));
|
|
}
|
|
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::BuildControlsPage(UVerticalBox* Parent)
|
|
{
|
|
AddSectionIntro(
|
|
Parent,
|
|
TEXT("INPUT"),
|
|
TEXT("Fast when you want it, forgiving when you need it"),
|
|
TEXT("Choose the established WCA speedcubing layout, a mnemonic pair layout, or bind every action yourself. Clicking a key starts capture; Escape cancels."));
|
|
|
|
UHorizontalBox* ProfileRow = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("KeyboardProfileRow"));
|
|
Parent->AddChildToVerticalBox(ProfileRow);
|
|
UTextBlock* ProfileLabel = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass(),
|
|
TEXT("KeyboardProfileLabel"));
|
|
ProfileLabel->SetText(FText::FromString(TEXT("Keyboard layout")));
|
|
ProfileLabel->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
|
|
FSlateFontInfo LabelFont = ProfileLabel->GetFont();
|
|
LabelFont.Size = 15;
|
|
ProfileLabel->SetFont(LabelFont);
|
|
if (UHorizontalBoxSlot* LabelSlot = ProfileRow->AddChildToHorizontalBox(ProfileLabel))
|
|
{
|
|
LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
LabelSlot->SetVerticalAlignment(VAlign_Center);
|
|
}
|
|
KeyboardProfileCombo = WidgetTree->ConstructWidget<UComboBoxString>(
|
|
UComboBoxString::StaticClass(),
|
|
TEXT("KeyboardProfileCombo"));
|
|
KeyboardProfileCombo->AddOption(TEXT("WCA Pro"));
|
|
KeyboardProfileCombo->AddOption(TEXT("Mnemonic Pairs"));
|
|
KeyboardProfileCombo->AddOption(TEXT("Custom"));
|
|
KeyboardProfileCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleKeyboardProfileChanged);
|
|
if (UHorizontalBoxSlot* ComboSlot = ProfileRow->AddChildToHorizontalBox(KeyboardProfileCombo))
|
|
{
|
|
ComboSlot->SetPadding(FMargin(18.0f, 6.0f));
|
|
ComboSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
}
|
|
|
|
PointerSensitivitySlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Pointer sensitivity"),
|
|
TEXT("Controls picking and direct manipulation."),
|
|
TEXT("PointerSensitivitySlider"),
|
|
PointerSensitivityValue);
|
|
OrbitSensitivitySlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Orbit sensitivity"),
|
|
TEXT("Controls middle-mouse and Shift + right-mouse camera movement."),
|
|
TEXT("OrbitSensitivitySlider"),
|
|
OrbitSensitivityValue);
|
|
ZoomSensitivitySlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Zoom sensitivity"),
|
|
TEXT("Controls wheel and trackpad zoom speed."),
|
|
TEXT("ZoomSensitivitySlider"),
|
|
ZoomSensitivityValue);
|
|
PointerSensitivitySlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandlePointerSensitivityChanged);
|
|
OrbitSensitivitySlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleOrbitSensitivityChanged);
|
|
ZoomSensitivitySlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleZoomSensitivityChanged);
|
|
|
|
InvertOrbitCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Invert vertical orbit"),
|
|
TEXT("Reverses vertical camera drag without changing puzzle turns."),
|
|
TEXT("InvertOrbitCheck"));
|
|
TouchInputCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Enable touch input"),
|
|
TEXT("Keeps direct touch turns available on compatible displays."),
|
|
TEXT("TouchInputCheck"));
|
|
InvertOrbitCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleInvertOrbitChanged);
|
|
TouchInputCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleTouchInputChanged);
|
|
|
|
AddText(Parent, TEXT("KEY BINDINGS"), 13, WarmAccentColor);
|
|
AddText(
|
|
Parent,
|
|
TEXT("Conflicts are resolved by swapping the two keys, so no action silently disappears."),
|
|
13,
|
|
MutedTextColor);
|
|
|
|
BindingRows.Reset();
|
|
for (const FHyperTwistKeyBindingDescriptor& Descriptor :
|
|
UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(Preferences))
|
|
{
|
|
UHyperTwistKeyBindingRowWidget* Row =
|
|
WidgetTree->ConstructWidget<UHyperTwistKeyBindingRowWidget>(
|
|
UHyperTwistKeyBindingRowWidget::StaticClass());
|
|
Row->ConfigureBinding(
|
|
Descriptor.ActionId,
|
|
Descriptor.Label,
|
|
Descriptor.Category,
|
|
Descriptor.Key);
|
|
Row->OnRebindRequested.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleBindingRequested);
|
|
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
|
|
{
|
|
RowSlot->SetPadding(FMargin(0.0f, 2.0f));
|
|
}
|
|
BindingRows.Add(Row);
|
|
}
|
|
|
|
UHorizontalBox* ResetRow = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("SettingsResetRow"));
|
|
if (UVerticalBoxSlot* ResetRowSlot = Parent->AddChildToVerticalBox(ResetRow))
|
|
{
|
|
ResetRowSlot->SetPadding(FMargin(0.0f, 16.0f, 0.0f, 8.0f));
|
|
}
|
|
UButton* ResetButton = AddActionButton(
|
|
ResetRow,
|
|
TEXT("Restore all defaults"),
|
|
TEXT("SettingsResetDefaultsButton"));
|
|
ResetButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleResetDefaultsClicked);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::BuildAudioPage(UVerticalBox* Parent)
|
|
{
|
|
AddSectionIntro(
|
|
Parent,
|
|
TEXT("SOUND"),
|
|
TEXT("Hear the turn, keep the focus"),
|
|
TEXT("Balance puzzle feedback, music, spoken teaching, and overall output. Muting never changes saved channel levels."));
|
|
MasterVolumeSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Master volume"),
|
|
TEXT("Overall HyperTwist output."),
|
|
TEXT("MasterVolumeSlider"),
|
|
MasterVolumeValue);
|
|
MusicVolumeSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Music"),
|
|
TEXT("Ambient and menu music."),
|
|
TEXT("MusicVolumeSlider"),
|
|
MusicVolumeValue);
|
|
EffectsVolumeSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Puzzle effects"),
|
|
TEXT("Turns, confirmations, timers, and interface feedback."),
|
|
TEXT("EffectsVolumeSlider"),
|
|
EffectsVolumeValue);
|
|
VoiceVolumeSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Coach voice"),
|
|
TEXT("Narration and spoken guidance."),
|
|
TEXT("VoiceVolumeSlider"),
|
|
VoiceVolumeValue);
|
|
MasterVolumeSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleMasterVolumeChanged);
|
|
MusicVolumeSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleMusicVolumeChanged);
|
|
EffectsVolumeSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleEffectsVolumeChanged);
|
|
VoiceVolumeSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceVolumeChanged);
|
|
MutedCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Mute all sound"),
|
|
TEXT("Temporarily silences HyperTwist while preserving the mix above."),
|
|
TEXT("MutedCheck"));
|
|
MutedCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleMutedChanged);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::BuildGraphicsPage(UVerticalBox* Parent)
|
|
{
|
|
AddSectionIntro(
|
|
Parent,
|
|
TEXT("DISPLAY"),
|
|
TEXT("Clarity before spectacle"),
|
|
TEXT("Tune visual quality for the puzzle order and hardware you are using. High is the balanced default; larger 4D slices may benefit from Medium."));
|
|
|
|
GraphicsQualityCombo = WidgetTree->ConstructWidget<UComboBoxString>(
|
|
UComboBoxString::StaticClass(),
|
|
TEXT("GraphicsQualityCombo"));
|
|
const TArray<FString> QualityOptions = {
|
|
TEXT("Low"),
|
|
TEXT("Medium"),
|
|
TEXT("High"),
|
|
TEXT("Epic")
|
|
};
|
|
for (const FString& Option : QualityOptions)
|
|
{
|
|
GraphicsQualityCombo->AddOption(Option);
|
|
}
|
|
GraphicsQualityCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleGraphicsQualityChanged);
|
|
AddText(Parent, TEXT("Overall quality"), 15, PrimaryTextColor);
|
|
Parent->AddChildToVerticalBox(GraphicsQualityCombo);
|
|
|
|
WindowModeCombo = WidgetTree->ConstructWidget<UComboBoxString>(
|
|
UComboBoxString::StaticClass(),
|
|
TEXT("WindowModeCombo"));
|
|
const TArray<FString> WindowModeOptions = {
|
|
TEXT("Fullscreen"),
|
|
TEXT("Borderless"),
|
|
TEXT("Windowed")
|
|
};
|
|
for (const FString& Option : WindowModeOptions)
|
|
{
|
|
WindowModeCombo->AddOption(Option);
|
|
}
|
|
WindowModeCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleWindowModeChanged);
|
|
AddText(Parent, TEXT("Display mode"), 15, PrimaryTextColor);
|
|
Parent->AddChildToVerticalBox(WindowModeCombo);
|
|
|
|
ResolutionScaleSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Render scale"),
|
|
TEXT("Lower this to regain performance without shrinking the interface."),
|
|
TEXT("ResolutionScaleSlider"),
|
|
ResolutionScaleValue);
|
|
ResolutionScaleSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleResolutionScaleChanged);
|
|
VSyncCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Vertical sync"),
|
|
TEXT("Reduces tearing by synchronizing frames with the display."),
|
|
TEXT("VSyncCheck"));
|
|
VSyncCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVSyncChanged);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::BuildAccessibilityPage(UVerticalBox* Parent)
|
|
{
|
|
AddSectionIntro(
|
|
Parent,
|
|
TEXT("ACCESS"),
|
|
TEXT("A readable path through every dimension"),
|
|
TEXT("Scale interface text and reduce non-essential motion without changing puzzle state or timing."));
|
|
UiScaleSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Interface scale"),
|
|
TEXT("Scales menus, labels, and teaching surfaces from 80% to 140%."),
|
|
TEXT("UiScaleSlider"),
|
|
UiScaleValue);
|
|
UiScaleSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleUiScaleChanged);
|
|
ReducedMotionCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Reduce motion"),
|
|
TEXT("Shortens decorative transitions while preserving puzzle animation cues."),
|
|
TEXT("ReducedMotionCheck"));
|
|
HighContrastCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("High contrast"),
|
|
TEXT("Strengthens separation between panels, labels, and state colors."),
|
|
TEXT("HighContrastCheck"));
|
|
SubtitlesCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Subtitles"),
|
|
TEXT("Shows text alongside spoken coaching and voice feedback."),
|
|
TEXT("SubtitlesCheck"));
|
|
ReducedMotionCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleReducedMotionChanged);
|
|
HighContrastCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleHighContrastChanged);
|
|
SubtitlesCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSubtitlesChanged);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::BuildSpeechAndCoachPage(UVerticalBox* Parent)
|
|
{
|
|
AddSectionIntro(
|
|
Parent,
|
|
TEXT("VOICE & AI"),
|
|
TEXT("Optional intelligence, always under your control"),
|
|
TEXT("HyperTwist works without speech or cloud services. Local processing stays the default; remote providers require explicit consent, and secrets are protected for the current Windows account."));
|
|
|
|
SpeechInputCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Enable speech-to-text"),
|
|
TEXT("Registers the global dictation shortcut. Microphone capture occurs only while explicitly active."),
|
|
TEXT("SpeechInputCheck"));
|
|
CoachNarrationCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Coach narration"),
|
|
TEXT("Lets teaching surfaces speak when a configured voice provider is available."),
|
|
TEXT("CoachNarrationCheck"));
|
|
AssistantPanelEnabledCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Fixed in-game coach"),
|
|
TEXT("Makes the native assistant panel available in every puzzle. Use its configured shortcut to open or close it."),
|
|
TEXT("AssistantPanelEnabledCheck"));
|
|
AssistantPanelOpenByDefaultCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Open coach with each puzzle"),
|
|
TEXT("Shows the fixed assistant panel when a playable runtime begins."),
|
|
TEXT("AssistantPanelOpenByDefaultCheck"));
|
|
CoachAiEnabledCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Enable AI responses"),
|
|
TEXT("When off, the panel remains useful as a private built-in controls and puzzle guide."),
|
|
TEXT("CoachAiEnabledCheck"));
|
|
CloudProvidersCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Allow cloud providers"),
|
|
TEXT("Required before HyperTwist sends text, speech, or puzzle context to any non-loopback endpoint."),
|
|
TEXT("CloudProvidersCheck"));
|
|
SpeechInputCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechInputChanged);
|
|
CoachNarrationCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachNarrationChanged);
|
|
CloudProvidersCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCloudProvidersChanged);
|
|
AssistantPanelEnabledCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleAssistantPanelEnabledChanged);
|
|
AssistantPanelOpenByDefaultCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleAssistantPanelOpenByDefaultChanged);
|
|
CoachAiEnabledCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachAiEnabledChanged);
|
|
|
|
AddText(Parent, TEXT("SPEECH TO TEXT"), 13, WarmAccentColor);
|
|
SpeechReadinessText = AddText(
|
|
Parent,
|
|
TEXT("STATUS | Checking local speech setup..."),
|
|
11,
|
|
MutedTextColor,
|
|
TEXT("SpeechReadinessText"));
|
|
SpeechProviderCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Transcription provider"),
|
|
TEXT("Choose an on-device engine, local bridge, or explicit cloud service."),
|
|
TEXT("SpeechProviderCombo"),
|
|
HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
|
|
HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions()));
|
|
SpeechActivationModeCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Shortcut behavior"),
|
|
TEXT("Hold records while pressed; Toggle starts and stops on separate presses."),
|
|
TEXT("SpeechActivationModeCombo"),
|
|
{TEXT("Hold to talk"), TEXT("Toggle recording")});
|
|
SpeechDictationDestinationCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Dictation destination"),
|
|
TEXT("Draft lets you review the transcript; immediate send asks the fixed coach as soon as transcription completes."),
|
|
TEXT("SpeechDictationDestinationCombo"),
|
|
{TEXT("Coach draft"), TEXT("Ask coach immediately")});
|
|
SpeechLanguageCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Language"),
|
|
TEXT("Auto-detect, or bias recognition toward a selected language."),
|
|
TEXT("SpeechLanguageCombo"),
|
|
{
|
|
TEXT("Auto-detect"),
|
|
TEXT("English"),
|
|
TEXT("German"),
|
|
TEXT("French"),
|
|
TEXT("Spanish"),
|
|
TEXT("Italian"),
|
|
TEXT("Portuguese"),
|
|
TEXT("Japanese"),
|
|
TEXT("Korean"),
|
|
TEXT("Chinese")
|
|
});
|
|
SpeechModelInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Transcription model"),
|
|
TEXT("Provider-qualified model identifier or local model profile."),
|
|
TEXT("SpeechModelInput"));
|
|
AvailableMicrophones =
|
|
UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones();
|
|
TArray<FString> MicrophoneLabels;
|
|
for (const FHyperTwistMicrophoneDescriptor& Microphone : AvailableMicrophones)
|
|
{
|
|
MicrophoneLabels.AddUnique(Microphone.DisplayName);
|
|
}
|
|
SpeechMicrophoneCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Microphone"),
|
|
TEXT("Choose a detected Windows input device. System default follows the current operating-system selection."),
|
|
TEXT("SpeechMicrophoneCombo"),
|
|
MicrophoneLabels);
|
|
SpeechContextInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Recognition context"),
|
|
TEXT("Puzzle names and specialist terms that should bias transcription accuracy."),
|
|
TEXT("SpeechContextInput"));
|
|
SpeechEndpointInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Endpoint"),
|
|
TEXT("Loopback HTTP is allowed. Remote services must use HTTPS and cloud consent."),
|
|
TEXT("SpeechEndpointInput"));
|
|
SpeechCleanupCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Clean up dictated text"),
|
|
TEXT("Applies punctuation and configured vocabulary normalization."),
|
|
TEXT("SpeechCleanupCheck"));
|
|
SpeechSoundFeedbackCheck = AddCheckRow(
|
|
Parent,
|
|
TEXT("Recording sound feedback"),
|
|
TEXT("Plays a short cue when dictation starts and stops."),
|
|
TEXT("SpeechSoundFeedbackCheck"));
|
|
SpeechCredentialInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("API key"),
|
|
TEXT("Leave blank to keep the currently protected key."),
|
|
TEXT("SpeechCredentialInput"),
|
|
true);
|
|
UHorizontalBox* SpeechCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("SpeechCredentialButtonRow"));
|
|
Parent->AddChildToVerticalBox(SpeechCredentialRow);
|
|
UButton* SpeechSaveButton = AddActionButton(
|
|
SpeechCredentialRow,
|
|
TEXT("Save protected key"),
|
|
TEXT("SpeechCredentialSaveButton"));
|
|
UButton* SpeechRemoveButton = AddActionButton(
|
|
SpeechCredentialRow,
|
|
TEXT("Remove key"),
|
|
TEXT("SpeechCredentialRemoveButton"));
|
|
UButton* SpeechValidateButton = AddActionButton(
|
|
SpeechCredentialRow,
|
|
TEXT("Check configuration"),
|
|
TEXT("SpeechConfigurationValidateButton"));
|
|
UButton* SpeechRefreshMicrophonesButton = AddActionButton(
|
|
SpeechCredentialRow,
|
|
TEXT("Refresh microphones"),
|
|
TEXT("SpeechRefreshMicrophonesButton"));
|
|
UButton* SpeechTestMicrophoneButton = AddActionButton(
|
|
SpeechCredentialRow,
|
|
TEXT("Test microphone"),
|
|
TEXT("SpeechTestMicrophoneButton"));
|
|
SpeechSaveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSaveSpeechCredential);
|
|
SpeechRemoveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleRemoveSpeechCredential);
|
|
SpeechValidateButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleValidateSpeechConfiguration);
|
|
SpeechRefreshMicrophonesButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleRefreshMicrophones);
|
|
SpeechTestMicrophoneButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleTestMicrophone);
|
|
|
|
AddText(Parent, TEXT("TEXT TO SPEECH"), 13, WarmAccentColor);
|
|
VoiceReadinessText = AddText(
|
|
Parent,
|
|
TEXT("STATUS | Checking voice setup..."),
|
|
11,
|
|
MutedTextColor,
|
|
TEXT("VoiceReadinessText"));
|
|
VoiceProviderCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Voice provider"),
|
|
TEXT("On-device Piper is the private default; cloud voices remain optional."),
|
|
TEXT("VoiceProviderCombo"),
|
|
HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
|
|
HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions()));
|
|
VoiceModelInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Voice model"),
|
|
TEXT("Model identifier used by the selected voice service."),
|
|
TEXT("VoiceModelInput"));
|
|
VoiceIdInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Voice"),
|
|
TEXT("Provider voice identifier, for example en_US-lessac-medium or alloy."),
|
|
TEXT("VoiceIdInput"));
|
|
VoiceSpeedSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Speaking speed"),
|
|
TEXT("Adjust narration from 0.5x to 2.0x without changing subtitles."),
|
|
TEXT("VoiceSpeedSlider"),
|
|
VoiceSpeedValue);
|
|
VoiceEndpointInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Endpoint"),
|
|
TEXT("Loopback HTTP is allowed. Remote services must use HTTPS and cloud consent."),
|
|
TEXT("VoiceEndpointInput"));
|
|
VoiceCredentialInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("API key"),
|
|
TEXT("Leave blank to keep the currently protected key."),
|
|
TEXT("VoiceCredentialInput"),
|
|
true);
|
|
UHorizontalBox* VoiceCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("VoiceCredentialButtonRow"));
|
|
Parent->AddChildToVerticalBox(VoiceCredentialRow);
|
|
UButton* VoiceSaveButton = AddActionButton(
|
|
VoiceCredentialRow,
|
|
TEXT("Save protected key"),
|
|
TEXT("VoiceCredentialSaveButton"));
|
|
UButton* VoiceRemoveButton = AddActionButton(
|
|
VoiceCredentialRow,
|
|
TEXT("Remove key"),
|
|
TEXT("VoiceCredentialRemoveButton"));
|
|
UButton* VoiceValidateButton = AddActionButton(
|
|
VoiceCredentialRow,
|
|
TEXT("Check configuration"),
|
|
TEXT("VoiceConfigurationValidateButton"));
|
|
VoiceSaveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSaveVoiceCredential);
|
|
VoiceRemoveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleRemoveVoiceCredential);
|
|
VoiceValidateButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleValidateVoiceConfiguration);
|
|
|
|
AddText(Parent, TEXT("AI COACH"), 13, WarmAccentColor);
|
|
CoachReadinessText = AddText(
|
|
Parent,
|
|
TEXT("STATUS | Private built-in guide ready."),
|
|
11,
|
|
MutedTextColor,
|
|
TEXT("CoachReadinessText"));
|
|
CoachProviderCombo = AddComboSetting(
|
|
Parent,
|
|
TEXT("Provider"),
|
|
TEXT("The built-in guide works without AI. Select a model provider only for generated responses."),
|
|
TEXT("CoachProviderCombo"),
|
|
HyperTwistSettingsPanelWidgetInternal::BuildProviderLabels(
|
|
HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions()));
|
|
CoachEndpointInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Endpoint"),
|
|
TEXT("OpenAI Responses or OpenAI-compatible chat-completions endpoint."),
|
|
TEXT("CoachEndpointInput"));
|
|
CoachModelInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Model"),
|
|
TEXT("Optional provider model identifier."),
|
|
TEXT("CoachModelInput"));
|
|
CoachInstructionsInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("Coach instructions"),
|
|
TEXT("Bounded system guidance sent with AI requests. Live puzzle state is never invented."),
|
|
TEXT("CoachInstructionsInput"));
|
|
CoachTemperatureSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Response creativity"),
|
|
TEXT("Lower values favor repeatable technical guidance."),
|
|
TEXT("CoachTemperatureSlider"),
|
|
CoachTemperatureValue);
|
|
CoachMaxResponseTokensSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Response length"),
|
|
TEXT("Bounds generated coach answers between 128 and 2,048 tokens."),
|
|
TEXT("CoachMaxResponseTokensSlider"),
|
|
CoachMaxResponseTokensValue);
|
|
CoachRequestTimeoutSlider = AddSliderRow(
|
|
Parent,
|
|
TEXT("Request timeout"),
|
|
TEXT("Stops an unavailable coach request after 5 to 60 seconds."),
|
|
TEXT("CoachRequestTimeoutSlider"),
|
|
CoachRequestTimeoutValue);
|
|
CoachCredentialInput = AddTextSetting(
|
|
Parent,
|
|
TEXT("API key"),
|
|
TEXT("Leave blank to keep the currently protected key."),
|
|
TEXT("CoachCredentialInput"),
|
|
true);
|
|
UHorizontalBox* CoachCredentialRow = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass(),
|
|
TEXT("CoachCredentialButtonRow"));
|
|
Parent->AddChildToVerticalBox(CoachCredentialRow);
|
|
UButton* CoachSaveButton = AddActionButton(
|
|
CoachCredentialRow,
|
|
TEXT("Save protected key"),
|
|
TEXT("CoachCredentialSaveButton"),
|
|
true);
|
|
UButton* CoachRemoveButton = AddActionButton(
|
|
CoachCredentialRow,
|
|
TEXT("Remove key"),
|
|
TEXT("CoachCredentialRemoveButton"));
|
|
UButton* CoachValidateButton = AddActionButton(
|
|
CoachCredentialRow,
|
|
TEXT("Check configuration"),
|
|
TEXT("CoachConfigurationValidateButton"));
|
|
CoachSaveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSaveCoachCredential);
|
|
CoachRemoveButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleRemoveCoachCredential);
|
|
CoachValidateButton->OnClicked.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleValidateCoachConfiguration);
|
|
|
|
SpeechProviderCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechProviderChanged);
|
|
SpeechActivationModeCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechActivationModeChanged);
|
|
SpeechDictationDestinationCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechDictationDestinationChanged);
|
|
SpeechLanguageCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechLanguageChanged);
|
|
SpeechEndpointInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechEndpointCommitted);
|
|
SpeechModelInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechModelCommitted);
|
|
SpeechMicrophoneCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechMicrophoneChanged);
|
|
SpeechContextInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechContextCommitted);
|
|
SpeechCleanupCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechCleanupChanged);
|
|
SpeechSoundFeedbackCheck->OnCheckStateChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleSpeechSoundFeedbackChanged);
|
|
VoiceProviderCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceProviderChanged);
|
|
VoiceEndpointInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceEndpointCommitted);
|
|
VoiceModelInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceModelCommitted);
|
|
VoiceIdInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceIdCommitted);
|
|
VoiceSpeedSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleVoiceSpeedChanged);
|
|
CoachProviderCombo->OnSelectionChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachProviderChanged);
|
|
CoachEndpointInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachEndpointCommitted);
|
|
CoachModelInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachModelCommitted);
|
|
CoachInstructionsInput->OnTextCommitted.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachInstructionsCommitted);
|
|
CoachTemperatureSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachTemperatureChanged);
|
|
CoachMaxResponseTokensSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachMaxResponseTokensChanged);
|
|
CoachRequestTimeoutSlider->OnValueChanged.AddDynamic(
|
|
this,
|
|
&UHyperTwistSettingsPanelWidget::HandleCoachRequestTimeoutChanged);
|
|
}
|
|
|
|
UTextBlock* UHyperTwistSettingsPanelWidget::AddText(
|
|
UVerticalBox* Parent,
|
|
const FString& Text,
|
|
const int32 FontSize,
|
|
const FLinearColor& Color,
|
|
const FName& WidgetName
|
|
)
|
|
{
|
|
if (Parent == nullptr || WidgetTree == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
UTextBlock* TextBlock = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass(),
|
|
WidgetName);
|
|
TextBlock->SetText(FText::FromString(Text));
|
|
TextBlock->SetAutoWrapText(true);
|
|
TextBlock->SetColorAndOpacity(FSlateColor(Color));
|
|
FSlateFontInfo Font = TextBlock->GetFont();
|
|
Font.Size = FontSize;
|
|
TextBlock->SetFont(Font);
|
|
if (UVerticalBoxSlot* TextSlot = Parent->AddChildToVerticalBox(TextBlock))
|
|
{
|
|
TextSlot->SetPadding(FMargin(0.0f, 2.0f));
|
|
}
|
|
return TextBlock;
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::AddSectionIntro(
|
|
UVerticalBox* Parent,
|
|
const FString& Eyebrow,
|
|
const FString& Title,
|
|
const FString& Description
|
|
)
|
|
{
|
|
AddText(Parent, Eyebrow, 12, AccentColor);
|
|
AddText(Parent, Title, 24, PrimaryTextColor);
|
|
UTextBlock* DescriptionText = AddText(Parent, Description, 14, MutedTextColor);
|
|
if (UVerticalBoxSlot* DescriptionSlot = Cast<UVerticalBoxSlot>(
|
|
DescriptionText != nullptr ? DescriptionText->Slot : nullptr))
|
|
{
|
|
DescriptionSlot->SetPadding(FMargin(0.0f, 3.0f, 0.0f, 18.0f));
|
|
}
|
|
}
|
|
|
|
UButton* UHyperTwistSettingsPanelWidget::AddTabButton(
|
|
UUniformGridPanel* Parent,
|
|
const FString& Label,
|
|
const int32 Column,
|
|
const FName& WidgetName
|
|
)
|
|
{
|
|
UButton* Button = WidgetTree->ConstructWidget<UButton>(
|
|
UButton::StaticClass(),
|
|
WidgetName);
|
|
Button->SetBackgroundColor(CardColor);
|
|
UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass());
|
|
LabelText->SetText(FText::FromString(Label));
|
|
LabelText->SetJustification(ETextJustify::Center);
|
|
LabelText->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
|
|
FSlateFontInfo Font = LabelText->GetFont();
|
|
Font.Size = 13;
|
|
LabelText->SetFont(Font);
|
|
Button->AddChild(LabelText);
|
|
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(LabelText->Slot))
|
|
{
|
|
ButtonSlot->SetPadding(FMargin(10.0f, 8.0f));
|
|
ButtonSlot->SetHorizontalAlignment(HAlign_Center);
|
|
}
|
|
if (UUniformGridSlot* GridSlot = Parent->AddChildToUniformGrid(Button, 0, Column))
|
|
{
|
|
GridSlot->SetHorizontalAlignment(HAlign_Fill);
|
|
}
|
|
return Button;
|
|
}
|
|
|
|
UButton* UHyperTwistSettingsPanelWidget::AddActionButton(
|
|
UHorizontalBox* Parent,
|
|
const FString& Label,
|
|
const FName& WidgetName,
|
|
const bool bPrimary
|
|
)
|
|
{
|
|
UButton* Button = WidgetTree->ConstructWidget<UButton>(
|
|
UButton::StaticClass(),
|
|
WidgetName);
|
|
Button->SetBackgroundColor(bPrimary ? AccentColor : CardColor);
|
|
UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass());
|
|
LabelText->SetText(FText::FromString(Label));
|
|
LabelText->SetColorAndOpacity(FSlateColor(
|
|
bPrimary ? FLinearColor(0.01f, 0.04f, 0.05f, 1.0f) : PrimaryTextColor));
|
|
FSlateFontInfo Font = LabelText->GetFont();
|
|
Font.Size = 14;
|
|
LabelText->SetFont(Font);
|
|
Button->AddChild(LabelText);
|
|
if (UButtonSlot* ButtonSlot = Cast<UButtonSlot>(LabelText->Slot))
|
|
{
|
|
ButtonSlot->SetPadding(FMargin(18.0f, 9.0f));
|
|
ButtonSlot->SetHorizontalAlignment(HAlign_Center);
|
|
}
|
|
if (UHorizontalBoxSlot* ActionSlot = Parent->AddChildToHorizontalBox(Button))
|
|
{
|
|
ActionSlot->SetPadding(FMargin(4.0f));
|
|
ActionSlot->SetVerticalAlignment(VAlign_Center);
|
|
}
|
|
return Button;
|
|
}
|
|
|
|
USlider* UHyperTwistSettingsPanelWidget::AddSliderRow(
|
|
UVerticalBox* Parent,
|
|
const FString& Label,
|
|
const FString& SupportingText,
|
|
const FName& WidgetName,
|
|
TObjectPtr<UTextBlock>& OutValueLabel
|
|
)
|
|
{
|
|
UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass());
|
|
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
|
|
{
|
|
RowSlot->SetPadding(FMargin(0.0f, 8.0f));
|
|
}
|
|
UHorizontalBox* Header = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass());
|
|
Row->AddChildToVerticalBox(Header);
|
|
UTextBlock* LabelText = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass());
|
|
LabelText->SetText(FText::FromString(Label));
|
|
LabelText->SetColorAndOpacity(FSlateColor(PrimaryTextColor));
|
|
FSlateFontInfo Font = LabelText->GetFont();
|
|
Font.Size = 15;
|
|
LabelText->SetFont(Font);
|
|
if (UHorizontalBoxSlot* LabelSlot = Header->AddChildToHorizontalBox(LabelText))
|
|
{
|
|
LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
}
|
|
OutValueLabel = WidgetTree->ConstructWidget<UTextBlock>(
|
|
UTextBlock::StaticClass());
|
|
OutValueLabel->SetColorAndOpacity(FSlateColor(AccentColor));
|
|
if (UHorizontalBoxSlot* ValueSlot = Header->AddChildToHorizontalBox(OutValueLabel))
|
|
{
|
|
ValueSlot->SetHorizontalAlignment(HAlign_Right);
|
|
}
|
|
AddText(Row, SupportingText, 12, MutedTextColor);
|
|
USlider* Slider = WidgetTree->ConstructWidget<USlider>(
|
|
USlider::StaticClass(),
|
|
WidgetName);
|
|
Slider->SetMinValue(0.0f);
|
|
Slider->SetMaxValue(1.0f);
|
|
Slider->SetStepSize(0.01f);
|
|
if (UVerticalBoxSlot* SliderSlot = Row->AddChildToVerticalBox(Slider))
|
|
{
|
|
SliderSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
|
|
}
|
|
return Slider;
|
|
}
|
|
|
|
UCheckBox* UHyperTwistSettingsPanelWidget::AddCheckRow(
|
|
UVerticalBox* Parent,
|
|
const FString& Label,
|
|
const FString& SupportingText,
|
|
const FName& WidgetName
|
|
)
|
|
{
|
|
UHorizontalBox* Row = WidgetTree->ConstructWidget<UHorizontalBox>(
|
|
UHorizontalBox::StaticClass());
|
|
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
|
|
{
|
|
RowSlot->SetPadding(FMargin(0.0f, 8.0f));
|
|
}
|
|
UVerticalBox* Copy = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass());
|
|
if (UHorizontalBoxSlot* CopySlot = Row->AddChildToHorizontalBox(Copy))
|
|
{
|
|
CopySlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill));
|
|
}
|
|
AddText(Copy, Label, 15, PrimaryTextColor);
|
|
AddText(Copy, SupportingText, 12, MutedTextColor);
|
|
UCheckBox* CheckBox = WidgetTree->ConstructWidget<UCheckBox>(
|
|
UCheckBox::StaticClass(),
|
|
WidgetName);
|
|
if (UHorizontalBoxSlot* CheckSlot = Row->AddChildToHorizontalBox(CheckBox))
|
|
{
|
|
CheckSlot->SetPadding(FMargin(18.0f, 0.0f));
|
|
CheckSlot->SetVerticalAlignment(VAlign_Center);
|
|
}
|
|
return CheckBox;
|
|
}
|
|
|
|
UEditableTextBox* UHyperTwistSettingsPanelWidget::AddTextSetting(
|
|
UVerticalBox* Parent,
|
|
const FString& Label,
|
|
const FString& SupportingText,
|
|
const FName& WidgetName,
|
|
const bool bPassword
|
|
)
|
|
{
|
|
UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass());
|
|
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
|
|
{
|
|
RowSlot->SetPadding(FMargin(0.0f, 8.0f));
|
|
}
|
|
AddText(Row, Label, 15, PrimaryTextColor);
|
|
AddText(Row, SupportingText, 12, MutedTextColor);
|
|
UEditableTextBox* Input = WidgetTree->ConstructWidget<UEditableTextBox>(
|
|
UEditableTextBox::StaticClass(),
|
|
WidgetName);
|
|
Input->SetIsPassword(bPassword);
|
|
Input->SetForegroundColor(PrimaryTextColor);
|
|
if (UVerticalBoxSlot* InputSlot = Row->AddChildToVerticalBox(Input))
|
|
{
|
|
InputSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
|
|
}
|
|
return Input;
|
|
}
|
|
|
|
UComboBoxString* UHyperTwistSettingsPanelWidget::AddComboSetting(
|
|
UVerticalBox* Parent,
|
|
const FString& Label,
|
|
const FString& SupportingText,
|
|
const FName& WidgetName,
|
|
const TArray<FString>& Options
|
|
)
|
|
{
|
|
UVerticalBox* Row = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass());
|
|
if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row))
|
|
{
|
|
RowSlot->SetPadding(FMargin(0.0f, 8.0f));
|
|
}
|
|
AddText(Row, Label, 15, PrimaryTextColor);
|
|
AddText(Row, SupportingText, 12, MutedTextColor);
|
|
UComboBoxString* Combo = WidgetTree->ConstructWidget<UComboBoxString>(
|
|
UComboBoxString::StaticClass(),
|
|
WidgetName);
|
|
for (const FString& Option : Options)
|
|
{
|
|
Combo->AddOption(Option);
|
|
}
|
|
if (UVerticalBoxSlot* ComboSlot = Row->AddChildToVerticalBox(Combo))
|
|
{
|
|
ComboSlot->SetPadding(FMargin(0.0f, 6.0f, 0.0f, 0.0f));
|
|
}
|
|
return Combo;
|
|
}
|
|
|
|
UVerticalBox* UHyperTwistSettingsPanelWidget::AddScrollablePage(
|
|
const FName& WidgetName
|
|
)
|
|
{
|
|
UScrollBox* Scroll = WidgetTree->ConstructWidget<UScrollBox>(
|
|
UScrollBox::StaticClass(),
|
|
WidgetName);
|
|
SettingsPageSwitcher->AddChild(Scroll);
|
|
UVerticalBox* Page = WidgetTree->ConstructWidget<UVerticalBox>(
|
|
UVerticalBox::StaticClass());
|
|
Scroll->AddChild(Page);
|
|
return Page;
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::RefreshWidgetsFromPreferences()
|
|
{
|
|
if (WidgetTree == nullptr || WidgetTree->RootWidget == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
bRefreshingWidgets = true;
|
|
|
|
if (KeyboardProfileCombo != nullptr)
|
|
{
|
|
KeyboardProfileCombo->SetSelectedOption(
|
|
UHyperTwistPlayerSettingsLibrary::GetKeyboardProfileDisplayName(
|
|
Preferences.KeyboardProfileId));
|
|
}
|
|
if (PointerSensitivitySlider != nullptr)
|
|
{
|
|
PointerSensitivitySlider->SetValue((Preferences.PointerSensitivity - 0.2f) / 2.8f);
|
|
}
|
|
if (OrbitSensitivitySlider != nullptr)
|
|
{
|
|
OrbitSensitivitySlider->SetValue((Preferences.OrbitSensitivity - 0.2f) / 2.8f);
|
|
}
|
|
if (ZoomSensitivitySlider != nullptr)
|
|
{
|
|
ZoomSensitivitySlider->SetValue((Preferences.ZoomSensitivity - 0.2f) / 2.8f);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
PointerSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.PointerSensitivity));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
OrbitSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.OrbitSensitivity));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
ZoomSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.ZoomSensitivity));
|
|
if (InvertOrbitCheck != nullptr)
|
|
{
|
|
InvertOrbitCheck->SetIsChecked(Preferences.bInvertOrbitY);
|
|
}
|
|
if (TouchInputCheck != nullptr)
|
|
{
|
|
TouchInputCheck->SetIsChecked(Preferences.bTouchInputEnabled);
|
|
}
|
|
|
|
const TArray<TPair<USlider*, float>> AudioSliders = {
|
|
{MasterVolumeSlider, Preferences.MasterVolume},
|
|
{MusicVolumeSlider, Preferences.MusicVolume},
|
|
{EffectsVolumeSlider, Preferences.EffectsVolume},
|
|
{VoiceVolumeSlider, Preferences.VoiceVolume}
|
|
};
|
|
for (const TPair<USlider*, float>& Entry : AudioSliders)
|
|
{
|
|
if (Entry.Key != nullptr)
|
|
{
|
|
Entry.Key->SetValue(Entry.Value);
|
|
}
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
MasterVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.MasterVolume));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
MusicVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.MusicVolume));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
EffectsVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.EffectsVolume));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
VoiceVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Preferences.VoiceVolume));
|
|
if (MutedCheck != nullptr)
|
|
{
|
|
MutedCheck->SetIsChecked(Preferences.bMuted);
|
|
}
|
|
|
|
if (GraphicsQualityCombo != nullptr)
|
|
{
|
|
GraphicsQualityCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::QualityLabel(Preferences.GraphicsQuality));
|
|
}
|
|
if (WindowModeCombo != nullptr)
|
|
{
|
|
WindowModeCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::WindowModeLabel(Preferences.WindowMode));
|
|
}
|
|
if (ResolutionScaleSlider != nullptr)
|
|
{
|
|
ResolutionScaleSlider->SetValue((Preferences.ResolutionScalePercent - 50.0f) / 50.0f);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
ResolutionScaleValue,
|
|
FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.ResolutionScalePercent)));
|
|
if (VSyncCheck != nullptr)
|
|
{
|
|
VSyncCheck->SetIsChecked(Preferences.bVSyncEnabled);
|
|
}
|
|
|
|
if (UiScaleSlider != nullptr)
|
|
{
|
|
UiScaleSlider->SetValue((Preferences.UiScale - 0.8f) / 0.6f);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
UiScaleValue,
|
|
FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.UiScale * 100.0f)));
|
|
if (ReducedMotionCheck != nullptr)
|
|
{
|
|
ReducedMotionCheck->SetIsChecked(Preferences.bReducedMotion);
|
|
}
|
|
if (HighContrastCheck != nullptr)
|
|
{
|
|
HighContrastCheck->SetIsChecked(Preferences.bHighContrast);
|
|
}
|
|
if (SubtitlesCheck != nullptr)
|
|
{
|
|
SubtitlesCheck->SetIsChecked(Preferences.bSubtitlesEnabled);
|
|
}
|
|
|
|
if (SpeechInputCheck != nullptr)
|
|
{
|
|
SpeechInputCheck->SetIsChecked(Preferences.bSpeechInputEnabled);
|
|
}
|
|
if (CoachNarrationCheck != nullptr)
|
|
{
|
|
CoachNarrationCheck->SetIsChecked(Preferences.bCoachNarrationEnabled);
|
|
}
|
|
if (CloudProvidersCheck != nullptr)
|
|
{
|
|
CloudProvidersCheck->SetIsChecked(Preferences.bAllowCloudProviders);
|
|
}
|
|
if (AssistantPanelEnabledCheck != nullptr)
|
|
{
|
|
AssistantPanelEnabledCheck->SetIsChecked(Preferences.bAssistantPanelEnabled);
|
|
}
|
|
if (AssistantPanelOpenByDefaultCheck != nullptr)
|
|
{
|
|
AssistantPanelOpenByDefaultCheck->SetIsChecked(
|
|
Preferences.bAssistantPanelOpenByDefault);
|
|
}
|
|
if (CoachAiEnabledCheck != nullptr)
|
|
{
|
|
CoachAiEnabledCheck->SetIsChecked(Preferences.bCoachAiEnabled);
|
|
}
|
|
if (SpeechProviderCombo != nullptr)
|
|
{
|
|
SpeechProviderCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions(),
|
|
Preferences.SpeechProviderId));
|
|
}
|
|
if (SpeechActivationModeCombo != nullptr)
|
|
{
|
|
SpeechActivationModeCombo->SetSelectedOption(
|
|
Preferences.SpeechActivationMode == TEXT("toggle")
|
|
? TEXT("Toggle recording")
|
|
: TEXT("Hold to talk"));
|
|
}
|
|
if (SpeechDictationDestinationCombo != nullptr)
|
|
{
|
|
SpeechDictationDestinationCombo->SetSelectedOption(
|
|
Preferences.SpeechDictationDestination == TEXT("coach-send")
|
|
? TEXT("Ask coach immediately")
|
|
: TEXT("Coach draft"));
|
|
}
|
|
if (SpeechLanguageCombo != nullptr)
|
|
{
|
|
SpeechLanguageCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::SpeechLanguageLabel(
|
|
Preferences.SpeechLanguage));
|
|
}
|
|
if (SpeechEndpointInput != nullptr)
|
|
{
|
|
SpeechEndpointInput->SetText(FText::FromString(Preferences.SpeechEndpoint));
|
|
}
|
|
if (SpeechModelInput != nullptr)
|
|
{
|
|
SpeechModelInput->SetText(FText::FromString(Preferences.SpeechModel));
|
|
}
|
|
if (SpeechMicrophoneCombo != nullptr)
|
|
{
|
|
const FHyperTwistMicrophoneDescriptor* SelectedMicrophone =
|
|
AvailableMicrophones.FindByPredicate(
|
|
[this](const FHyperTwistMicrophoneDescriptor& Microphone)
|
|
{
|
|
return Microphone.DeviceId.Equals(
|
|
Preferences.SpeechMicrophoneId,
|
|
ESearchCase::IgnoreCase);
|
|
});
|
|
const FString SelectedLabel = SelectedMicrophone != nullptr
|
|
? SelectedMicrophone->DisplayName
|
|
: Preferences.SpeechMicrophoneId;
|
|
if (SpeechMicrophoneCombo->FindOptionIndex(SelectedLabel) == INDEX_NONE)
|
|
{
|
|
SpeechMicrophoneCombo->AddOption(SelectedLabel);
|
|
}
|
|
SpeechMicrophoneCombo->SetSelectedOption(SelectedLabel);
|
|
}
|
|
if (SpeechContextInput != nullptr)
|
|
{
|
|
SpeechContextInput->SetText(
|
|
FText::FromString(Preferences.SpeechRecognitionContext));
|
|
}
|
|
if (SpeechCleanupCheck != nullptr)
|
|
{
|
|
SpeechCleanupCheck->SetIsChecked(Preferences.bSpeechCleanupEnabled);
|
|
}
|
|
if (SpeechSoundFeedbackCheck != nullptr)
|
|
{
|
|
SpeechSoundFeedbackCheck->SetIsChecked(
|
|
Preferences.bSpeechSoundFeedbackEnabled);
|
|
}
|
|
if (VoiceProviderCombo != nullptr)
|
|
{
|
|
VoiceProviderCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions(),
|
|
Preferences.VoiceProviderId));
|
|
}
|
|
if (VoiceEndpointInput != nullptr)
|
|
{
|
|
VoiceEndpointInput->SetText(FText::FromString(Preferences.VoiceEndpoint));
|
|
}
|
|
if (VoiceModelInput != nullptr)
|
|
{
|
|
VoiceModelInput->SetText(FText::FromString(Preferences.VoiceModel));
|
|
}
|
|
if (VoiceIdInput != nullptr)
|
|
{
|
|
VoiceIdInput->SetText(FText::FromString(Preferences.VoiceId));
|
|
}
|
|
if (VoiceSpeedSlider != nullptr)
|
|
{
|
|
VoiceSpeedSlider->SetValue((Preferences.VoiceSpeed - 0.5f) / 1.5f);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
VoiceSpeedValue,
|
|
FString::Printf(TEXT("%.2fx"), Preferences.VoiceSpeed));
|
|
if (CoachProviderCombo != nullptr)
|
|
{
|
|
CoachProviderCombo->SetSelectedOption(
|
|
HyperTwistSettingsPanelWidgetInternal::ProviderLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions(),
|
|
Preferences.CoachProviderId));
|
|
}
|
|
if (CoachEndpointInput != nullptr)
|
|
{
|
|
CoachEndpointInput->SetText(FText::FromString(Preferences.CoachEndpoint));
|
|
}
|
|
if (CoachModelInput != nullptr)
|
|
{
|
|
CoachModelInput->SetText(FText::FromString(Preferences.CoachModel));
|
|
}
|
|
if (CoachInstructionsInput != nullptr)
|
|
{
|
|
CoachInstructionsInput->SetText(
|
|
FText::FromString(Preferences.CoachSystemInstructions));
|
|
}
|
|
if (CoachTemperatureSlider != nullptr)
|
|
{
|
|
CoachTemperatureSlider->SetValue(Preferences.CoachTemperature);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachTemperatureValue,
|
|
FString::Printf(TEXT("%.2f"), Preferences.CoachTemperature));
|
|
if (CoachMaxResponseTokensSlider != nullptr)
|
|
{
|
|
CoachMaxResponseTokensSlider->SetValue(
|
|
static_cast<float>(Preferences.CoachMaxResponseTokens - 128)
|
|
/ static_cast<float>(2048 - 128));
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachMaxResponseTokensValue,
|
|
FString::Printf(TEXT("%d tokens"), Preferences.CoachMaxResponseTokens));
|
|
if (CoachRequestTimeoutSlider != nullptr)
|
|
{
|
|
CoachRequestTimeoutSlider->SetValue(
|
|
(Preferences.CoachRequestTimeoutSeconds - 5.0f) / 55.0f);
|
|
}
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachRequestTimeoutValue,
|
|
FString::Printf(
|
|
TEXT("%.0f seconds"),
|
|
Preferences.CoachRequestTimeoutSeconds));
|
|
RefreshBindingRows();
|
|
bRefreshingWidgets = false;
|
|
RefreshProviderReadiness();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::RefreshBindingRows()
|
|
{
|
|
const TArray<FHyperTwistKeyBindingDescriptor> Descriptors =
|
|
UHyperTwistPlayerSettingsLibrary::BuildKeyBindingDescriptors(Preferences);
|
|
for (UHyperTwistKeyBindingRowWidget* Row : BindingRows)
|
|
{
|
|
if (Row == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
const int32 RowIndex = BindingRows.IndexOfByKey(Row);
|
|
if (Descriptors.IsValidIndex(RowIndex))
|
|
{
|
|
Row->ConfigureBinding(
|
|
Descriptors[RowIndex].ActionId,
|
|
Descriptors[RowIndex].Label,
|
|
Descriptors[RowIndex].Category,
|
|
Descriptors[RowIndex].Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::RefreshMicrophoneOptions()
|
|
{
|
|
if (SpeechMicrophoneCombo == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const bool bWasRefreshing = bRefreshingWidgets;
|
|
bRefreshingWidgets = true;
|
|
AvailableMicrophones =
|
|
UHyperTwistPlayerSettingsLibrary::GetAvailableMicrophones();
|
|
if (!AvailableMicrophones.ContainsByPredicate(
|
|
[this](const FHyperTwistMicrophoneDescriptor& Microphone)
|
|
{
|
|
return Microphone.DeviceId.Equals(
|
|
Preferences.SpeechMicrophoneId,
|
|
ESearchCase::IgnoreCase);
|
|
}))
|
|
{
|
|
FHyperTwistMicrophoneDescriptor ExistingSelection;
|
|
ExistingSelection.DeviceId = Preferences.SpeechMicrophoneId;
|
|
ExistingSelection.DisplayName = Preferences.SpeechMicrophoneId;
|
|
AvailableMicrophones.Add(MoveTemp(ExistingSelection));
|
|
}
|
|
|
|
SpeechMicrophoneCombo->ClearOptions();
|
|
FString SelectedLabel;
|
|
for (const FHyperTwistMicrophoneDescriptor& Microphone : AvailableMicrophones)
|
|
{
|
|
SpeechMicrophoneCombo->AddOption(Microphone.DisplayName);
|
|
if (Microphone.DeviceId.Equals(
|
|
Preferences.SpeechMicrophoneId,
|
|
ESearchCase::IgnoreCase))
|
|
{
|
|
SelectedLabel = Microphone.DisplayName;
|
|
}
|
|
}
|
|
SpeechMicrophoneCombo->SetSelectedOption(
|
|
SelectedLabel.IsEmpty()
|
|
? TEXT("System default microphone")
|
|
: SelectedLabel);
|
|
bRefreshingWidgets = bWasRefreshing;
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::RefreshProviderReadiness()
|
|
{
|
|
auto SetReadiness = [this](
|
|
UTextBlock* Target,
|
|
const FString& Message,
|
|
const bool bReady)
|
|
{
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(Target, Message);
|
|
if (Target != nullptr)
|
|
{
|
|
Target->SetColorAndOpacity(FSlateColor(
|
|
bReady ? AccentColor : WarmAccentColor));
|
|
}
|
|
};
|
|
|
|
FString FailureReason;
|
|
const bool bSpeechEndpointAllowed =
|
|
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.SpeechEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason);
|
|
const bool bSpeechCloud =
|
|
Preferences.SpeechProviderId == TEXT("openai")
|
|
|| Preferences.SpeechProviderId == TEXT("groq")
|
|
|| Preferences.SpeechProviderId == TEXT("deepgram")
|
|
|| Preferences.SpeechProviderId == TEXT("elevenlabs");
|
|
const bool bSpeechReady =
|
|
Preferences.bSpeechInputEnabled
|
|
&& bSpeechEndpointAllowed
|
|
&& !Preferences.SpeechModel.IsEmpty()
|
|
&& (!bSpeechCloud
|
|
|| (Preferences.bAllowCloudProviders
|
|
&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("speech-api-key"))));
|
|
SetReadiness(
|
|
SpeechReadinessText,
|
|
bSpeechReady
|
|
? TEXT("READY TO TRY | Shortcut, microphone, model, and privacy route configured.")
|
|
: Preferences.bSpeechInputEnabled
|
|
? TEXT("NEEDS SETUP | Complete the endpoint, model, consent, or protected key.")
|
|
: TEXT("OFF | Enable speech-to-text to register the global shortcut."),
|
|
bSpeechReady);
|
|
|
|
FailureReason.Reset();
|
|
const bool bVoiceEndpointAllowed =
|
|
UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.VoiceEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason);
|
|
const bool bVoiceCloud =
|
|
Preferences.VoiceProviderId == TEXT("openai")
|
|
|| Preferences.VoiceProviderId == TEXT("elevenlabs");
|
|
const bool bVoiceReady =
|
|
Preferences.bCoachNarrationEnabled
|
|
&& bVoiceEndpointAllowed
|
|
&& !Preferences.VoiceModel.IsEmpty()
|
|
&& !Preferences.VoiceId.IsEmpty()
|
|
&& (!bVoiceCloud
|
|
|| (Preferences.bAllowCloudProviders
|
|
&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("voice-api-key"))));
|
|
SetReadiness(
|
|
VoiceReadinessText,
|
|
bVoiceReady
|
|
? TEXT("READY TO TRY | Voice, model, endpoint, and privacy route configured.")
|
|
: Preferences.bCoachNarrationEnabled
|
|
? TEXT("NEEDS SETUP | Complete the voice, model, endpoint, consent, or protected key.")
|
|
: TEXT("OFF | Enable coach narration when spoken guidance is wanted."),
|
|
bVoiceReady);
|
|
|
|
const bool bBuiltInCoach =
|
|
!Preferences.bCoachAiEnabled
|
|
|| Preferences.CoachProviderId == TEXT("local-disabled");
|
|
FailureReason.Reset();
|
|
const bool bCoachEndpointAllowed = bBuiltInCoach
|
|
|| UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.CoachEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason);
|
|
const bool bCoachCloud =
|
|
Preferences.CoachProviderId == TEXT("openai")
|
|
|| Preferences.CoachProviderId == TEXT("groq");
|
|
const bool bCoachReady = bBuiltInCoach
|
|
|| (bCoachEndpointAllowed
|
|
&& !Preferences.CoachModel.IsEmpty()
|
|
&& (!bCoachCloud
|
|
|| (Preferences.bAllowCloudProviders
|
|
&& UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("coach-api-key")))));
|
|
SetReadiness(
|
|
CoachReadinessText,
|
|
bBuiltInCoach
|
|
? TEXT("READY | Private built-in guide; no provider or account required.")
|
|
: bCoachReady
|
|
? TEXT("READY TO TRY | AI model, endpoint, consent, and protected key configured.")
|
|
: TEXT("NEEDS SETUP | Complete the AI model, endpoint, consent, or protected key."),
|
|
bCoachReady);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::SavePreferencesAndApply(
|
|
const FString& StatusMessage,
|
|
const bool bApplyGraphics
|
|
)
|
|
{
|
|
if (!UHyperTwistPlayerSettingsLibrary::SavePreferences(Preferences))
|
|
{
|
|
SetStatus(TEXT("HyperTwist could not save settings on this device."), true);
|
|
return;
|
|
}
|
|
UHyperTwistPlayerSettingsLibrary::ApplyRuntimePreferences(
|
|
Preferences,
|
|
bApplyGraphics);
|
|
RefreshProviderReadiness();
|
|
SetStatus(StatusMessage);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::SetStatus(
|
|
const FString& Message,
|
|
const bool bError
|
|
)
|
|
{
|
|
if (StatusText != nullptr)
|
|
{
|
|
StatusText->SetText(FText::FromString(Message));
|
|
StatusText->SetColorAndOpacity(FSlateColor(
|
|
bError ? FLinearColor(1.0f, 0.38f, 0.30f, 1.0f) : MutedTextColor));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ShowControlsPage()
|
|
{
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::ControlsPageIndex);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ShowAudioPage()
|
|
{
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::AudioPageIndex);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ShowGraphicsPage()
|
|
{
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::GraphicsPageIndex);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ShowAccessibilityPage()
|
|
{
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::AccessibilityPageIndex);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::ShowSpeechAndCoachPage()
|
|
{
|
|
SettingsPageSwitcher->SetActiveWidgetIndex(
|
|
HyperTwistSettingsPanelWidgetInternal::SpeechPageIndex);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCloseClicked()
|
|
{
|
|
OnCloseRequested.Broadcast();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleResetDefaultsClicked()
|
|
{
|
|
Preferences = UHyperTwistPlayerSettingsLibrary::GetDefaultPreferences();
|
|
RefreshWidgetsFromPreferences();
|
|
SavePreferencesAndApply(TEXT("All settings restored to defaults."), true);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleBindingRequested(const FName ActionId)
|
|
{
|
|
PendingRebindActionId = ActionId;
|
|
SetStatus(TEXT("Press a keyboard or mouse key. Escape cancels."));
|
|
SetKeyboardFocus();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleKeyboardProfileChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
FString ProfileId;
|
|
if (SelectedItem == TEXT("WCA Pro"))
|
|
{
|
|
ProfileId = TEXT("classic-wca-keyboard/v1");
|
|
}
|
|
else if (SelectedItem == TEXT("Mnemonic Pairs"))
|
|
{
|
|
ProfileId = TEXT("classic-mnemonic-pairs/v1");
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
if (UHyperTwistPlayerSettingsLibrary::ApplyKeyboardProfile(Preferences, ProfileId))
|
|
{
|
|
RefreshBindingRows();
|
|
SavePreferencesAndApply(FString::Printf(TEXT("%s layout applied."), *SelectedItem));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandlePointerSensitivityChanged(const float Value)
|
|
{
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.PointerSensitivity = 0.2f + Value * 2.8f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
PointerSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.PointerSensitivity));
|
|
SavePreferencesAndApply(TEXT("Pointer sensitivity updated."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleOrbitSensitivityChanged(const float Value)
|
|
{
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.OrbitSensitivity = 0.2f + Value * 2.8f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
OrbitSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.OrbitSensitivity));
|
|
SavePreferencesAndApply(TEXT("Orbit sensitivity updated."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleZoomSensitivityChanged(const float Value)
|
|
{
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.ZoomSensitivity = 0.2f + Value * 2.8f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
ZoomSensitivityValue,
|
|
HyperTwistSettingsPanelWidgetInternal::DecimalLabel(Preferences.ZoomSensitivity));
|
|
SavePreferencesAndApply(TEXT("Zoom sensitivity updated."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleInvertOrbitChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bInvertOrbitY = bChecked;
|
|
SavePreferencesAndApply(TEXT("Orbit direction updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleTouchInputChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bTouchInputEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Touch input preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleMasterVolumeChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.MasterVolume = Value;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
MasterVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
|
|
SavePreferencesAndApply(TEXT("Master volume updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleMusicVolumeChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.MusicVolume = Value;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
MusicVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
|
|
SavePreferencesAndApply(TEXT("Music volume updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleEffectsVolumeChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.EffectsVolume = Value;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
EffectsVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
|
|
SavePreferencesAndApply(TEXT("Puzzle effects volume updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceVolumeChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.VoiceVolume = Value;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
VoiceVolumeValue,
|
|
HyperTwistSettingsPanelWidgetInternal::PercentLabel(Value));
|
|
SavePreferencesAndApply(TEXT("Coach voice volume updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleMutedChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bMuted = bChecked;
|
|
SavePreferencesAndApply(bChecked ? TEXT("Sound muted.") : TEXT("Sound restored."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleGraphicsQualityChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.GraphicsQuality = SelectedItem == TEXT("Low")
|
|
? EHyperTwistGraphicsQuality::Low
|
|
: SelectedItem == TEXT("Medium")
|
|
? EHyperTwistGraphicsQuality::Medium
|
|
: SelectedItem == TEXT("Epic")
|
|
? EHyperTwistGraphicsQuality::Epic
|
|
: EHyperTwistGraphicsQuality::High;
|
|
SavePreferencesAndApply(TEXT("Graphics quality applied."), true);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleWindowModeChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.WindowMode = SelectedItem == TEXT("Fullscreen")
|
|
? EHyperTwistWindowMode::Fullscreen
|
|
: SelectedItem == TEXT("Windowed")
|
|
? EHyperTwistWindowMode::Windowed
|
|
: EHyperTwistWindowMode::Borderless;
|
|
SavePreferencesAndApply(TEXT("Display mode applied."), true);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleResolutionScaleChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.ResolutionScalePercent = 50.0f + Value * 50.0f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
ResolutionScaleValue,
|
|
FString::Printf(
|
|
TEXT("%d%%"),
|
|
FMath::RoundToInt(Preferences.ResolutionScalePercent)));
|
|
SavePreferencesAndApply(TEXT("Render scale applied."), true);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVSyncChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bVSyncEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Vertical sync preference applied."), true);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleUiScaleChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.UiScale = 0.8f + Value * 0.6f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
UiScaleValue,
|
|
FString::Printf(TEXT("%d%%"), FMath::RoundToInt(Preferences.UiScale * 100.0f)));
|
|
SavePreferencesAndApply(TEXT("Interface scale applied."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleReducedMotionChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bReducedMotion = bChecked;
|
|
SavePreferencesAndApply(TEXT("Motion preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleHighContrastChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bHighContrast = bChecked;
|
|
SavePreferencesAndApply(TEXT("Contrast preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSubtitlesChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bSubtitlesEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Subtitle preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechInputChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bSpeechInputEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Speech input preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachNarrationChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bCoachNarrationEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Coach narration preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCloudProvidersChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bAllowCloudProviders = bChecked;
|
|
SavePreferencesAndApply(TEXT("Cloud provider preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleAssistantPanelEnabledChanged(
|
|
const bool bChecked
|
|
)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bAssistantPanelEnabled = bChecked;
|
|
if (!bChecked)
|
|
{
|
|
Preferences.bAssistantPanelOpenByDefault = false;
|
|
}
|
|
SavePreferencesAndApply(TEXT("In-game coach availability updated."));
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleAssistantPanelOpenByDefaultChanged(
|
|
const bool bChecked
|
|
)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bAssistantPanelOpenByDefault = bChecked;
|
|
if (bChecked)
|
|
{
|
|
Preferences.bAssistantPanelEnabled = true;
|
|
}
|
|
SavePreferencesAndApply(TEXT("Coach startup visibility updated."));
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachAiEnabledChanged(const bool bChecked)
|
|
{
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
if (bChecked
|
|
&& Preferences.CoachProviderId.Equals(
|
|
TEXT("local-disabled"),
|
|
ESearchCase::IgnoreCase))
|
|
{
|
|
Preferences.bCoachAiEnabled = false;
|
|
SetStatus(TEXT("Choose an AI provider before enabling generated responses."), true);
|
|
RefreshWidgetsFromPreferences();
|
|
return;
|
|
}
|
|
Preferences.bCoachAiEnabled = bChecked;
|
|
SavePreferencesAndApply(
|
|
bChecked
|
|
? TEXT("AI coach responses enabled.")
|
|
: TEXT("AI responses disabled; the built-in guide remains available."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechProviderChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
|
|
HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetSpeechProviderOptions(),
|
|
SelectedItem);
|
|
if (Option == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.SpeechProviderId = Option->Id;
|
|
Preferences.SpeechEndpoint = Option->DefaultEndpoint;
|
|
Preferences.SpeechModel = Option->DefaultModel;
|
|
SavePreferencesAndApply(TEXT("Transcription provider and safe defaults updated."));
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechActivationModeChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechActivationMode =
|
|
SelectedItem == TEXT("Toggle recording") ? TEXT("toggle") : TEXT("hold");
|
|
SavePreferencesAndApply(TEXT("Dictation shortcut behavior updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechDictationDestinationChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechDictationDestination =
|
|
SelectedItem == TEXT("Ask coach immediately")
|
|
? TEXT("coach-send")
|
|
: TEXT("coach-draft");
|
|
SavePreferencesAndApply(TEXT("Dictation destination updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechLanguageChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechLanguage =
|
|
HyperTwistSettingsPanelWidgetInternal::SpeechLanguageCode(SelectedItem);
|
|
SavePreferencesAndApply(TEXT("Transcription language updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechEndpointCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechEndpoint = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Speech endpoint saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechModelCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechModel = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Transcription model saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechMicrophoneChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FHyperTwistMicrophoneDescriptor* Microphone =
|
|
AvailableMicrophones.FindByPredicate(
|
|
[&SelectedItem](const FHyperTwistMicrophoneDescriptor& Candidate)
|
|
{
|
|
return Candidate.DisplayName.Equals(
|
|
SelectedItem,
|
|
ESearchCase::IgnoreCase);
|
|
});
|
|
if (Microphone == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.SpeechMicrophoneId = Microphone->DeviceId;
|
|
SavePreferencesAndApply(TEXT("Microphone preference saved."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechContextCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.SpeechRecognitionContext = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Recognition context saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechCleanupChanged(const bool bChecked)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bSpeechCleanupEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Dictation cleanup preference updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSpeechSoundFeedbackChanged(
|
|
const bool bChecked
|
|
)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.bSpeechSoundFeedbackEnabled = bChecked;
|
|
SavePreferencesAndApply(TEXT("Dictation sound feedback updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceProviderChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
|
|
HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetVoiceProviderOptions(),
|
|
SelectedItem);
|
|
if (Option == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.VoiceProviderId = Option->Id;
|
|
Preferences.VoiceEndpoint = Option->DefaultEndpoint;
|
|
Preferences.VoiceModel = Option->DefaultModel;
|
|
if (Preferences.VoiceProviderId == TEXT("openai"))
|
|
{
|
|
Preferences.VoiceId = TEXT("alloy");
|
|
}
|
|
else if (Preferences.VoiceProviderId == TEXT("local-piper"))
|
|
{
|
|
Preferences.VoiceId = TEXT("en_US-lessac-medium");
|
|
}
|
|
SavePreferencesAndApply(TEXT("Voice provider and safe defaults updated."));
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceEndpointCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.VoiceEndpoint = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Voice endpoint saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceModelCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.VoiceModel = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Voice model saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceIdCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.VoiceId = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Voice selection saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleVoiceSpeedChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.VoiceSpeed = 0.5f + Value * 1.5f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
VoiceSpeedValue,
|
|
FString::Printf(TEXT("%.2fx"), Preferences.VoiceSpeed));
|
|
SavePreferencesAndApply(TEXT("Narration speed updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachProviderChanged(
|
|
FString SelectedItem,
|
|
ESelectInfo::Type SelectionType
|
|
)
|
|
{
|
|
static_cast<void>(SelectionType);
|
|
if (bRefreshingWidgets)
|
|
{
|
|
return;
|
|
}
|
|
const HyperTwistSettingsPanelWidgetInternal::FProviderOption* Option =
|
|
HyperTwistSettingsPanelWidgetInternal::FindProviderByLabel(
|
|
HyperTwistSettingsPanelWidgetInternal::GetCoachProviderOptions(),
|
|
SelectedItem);
|
|
if (Option == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
Preferences.CoachProviderId = Option->Id;
|
|
Preferences.CoachEndpoint = Option->DefaultEndpoint;
|
|
Preferences.CoachModel = Option->DefaultModel;
|
|
if (Preferences.CoachProviderId == TEXT("local-disabled"))
|
|
{
|
|
Preferences.bCoachAiEnabled = false;
|
|
}
|
|
SavePreferencesAndApply(TEXT("Coach provider and safe defaults updated."));
|
|
RefreshWidgetsFromPreferences();
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachEndpointCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachEndpoint = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Coach endpoint saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachModelCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachModel = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Coach model saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachInstructionsCommitted(
|
|
const FText& Text,
|
|
ETextCommit::Type CommitMethod
|
|
)
|
|
{
|
|
static_cast<void>(CommitMethod);
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachSystemInstructions = Text.ToString().TrimStartAndEnd();
|
|
SavePreferencesAndApply(TEXT("Coach instructions saved."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachTemperatureChanged(const float Value)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachTemperature = Value;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachTemperatureValue,
|
|
FString::Printf(TEXT("%.2f"), Value));
|
|
SavePreferencesAndApply(TEXT("Coach response style updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachMaxResponseTokensChanged(
|
|
const float Value
|
|
)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachMaxResponseTokens = FMath::RoundToInt(
|
|
128.0f + FMath::Clamp(Value, 0.0f, 1.0f) * (2048.0f - 128.0f));
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachMaxResponseTokensValue,
|
|
FString::Printf(
|
|
TEXT("%d tokens"),
|
|
Preferences.CoachMaxResponseTokens));
|
|
SavePreferencesAndApply(TEXT("Coach response length updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleCoachRequestTimeoutChanged(
|
|
const float Value
|
|
)
|
|
{
|
|
if (!bRefreshingWidgets)
|
|
{
|
|
Preferences.CoachRequestTimeoutSeconds =
|
|
5.0f + FMath::Clamp(Value, 0.0f, 1.0f) * 55.0f;
|
|
HyperTwistSettingsPanelWidgetInternal::SetText(
|
|
CoachRequestTimeoutValue,
|
|
FString::Printf(
|
|
TEXT("%.0f seconds"),
|
|
Preferences.CoachRequestTimeoutSeconds));
|
|
SavePreferencesAndApply(TEXT("Coach request timeout updated."));
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSaveSpeechCredential()
|
|
{
|
|
FString FailureReason;
|
|
const FString Secret = SpeechCredentialInput != nullptr
|
|
? SpeechCredentialInput->GetText().ToString()
|
|
: FString();
|
|
if (Secret.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Enter a speech API key, or leave the protected key unchanged."));
|
|
return;
|
|
}
|
|
if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
|
|
TEXT("speech-api-key"),
|
|
Secret,
|
|
FailureReason))
|
|
{
|
|
SpeechCredentialInput->SetText(FText::GetEmpty());
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Speech API key protected for this Windows account."));
|
|
}
|
|
else
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleRemoveSpeechCredential()
|
|
{
|
|
UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("speech-api-key"));
|
|
if (SpeechCredentialInput != nullptr)
|
|
{
|
|
SpeechCredentialInput->SetText(FText::GetEmpty());
|
|
}
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Protected speech API key removed."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSaveVoiceCredential()
|
|
{
|
|
FString FailureReason;
|
|
const FString Secret = VoiceCredentialInput != nullptr
|
|
? VoiceCredentialInput->GetText().ToString()
|
|
: FString();
|
|
if (Secret.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Enter a voice API key, or leave the protected key unchanged."));
|
|
return;
|
|
}
|
|
if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
|
|
TEXT("voice-api-key"),
|
|
Secret,
|
|
FailureReason))
|
|
{
|
|
VoiceCredentialInput->SetText(FText::GetEmpty());
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Voice API key protected for this Windows account."));
|
|
}
|
|
else
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleRemoveVoiceCredential()
|
|
{
|
|
UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("voice-api-key"));
|
|
if (VoiceCredentialInput != nullptr)
|
|
{
|
|
VoiceCredentialInput->SetText(FText::GetEmpty());
|
|
}
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Protected voice API key removed."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleSaveCoachCredential()
|
|
{
|
|
FString FailureReason;
|
|
const FString Secret = CoachCredentialInput != nullptr
|
|
? CoachCredentialInput->GetText().ToString()
|
|
: FString();
|
|
if (Secret.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Enter a coach API key, or leave the protected key unchanged."));
|
|
return;
|
|
}
|
|
if (UHyperTwistPlayerSettingsLibrary::StoreProviderCredential(
|
|
TEXT("coach-api-key"),
|
|
Secret,
|
|
FailureReason))
|
|
{
|
|
CoachCredentialInput->SetText(FText::GetEmpty());
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Coach API key protected for this Windows account."));
|
|
}
|
|
else
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
}
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleRemoveCoachCredential()
|
|
{
|
|
UHyperTwistPlayerSettingsLibrary::DeleteProviderCredential(TEXT("coach-api-key"));
|
|
if (CoachCredentialInput != nullptr)
|
|
{
|
|
CoachCredentialInput->SetText(FText::GetEmpty());
|
|
}
|
|
RefreshProviderReadiness();
|
|
SetStatus(TEXT("Protected coach API key removed."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleRefreshMicrophones()
|
|
{
|
|
RefreshMicrophoneOptions();
|
|
SetStatus(FString::Printf(
|
|
TEXT("Detected %d microphone route%s."),
|
|
AvailableMicrophones.Num(),
|
|
AvailableMicrophones.Num() == 1 ? TEXT("") : TEXT("s")));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleTestMicrophone()
|
|
{
|
|
FString FailureReason;
|
|
if (UHyperTwistPlayerSettingsLibrary::TestMicrophoneDevice(
|
|
Preferences.SpeechMicrophoneId,
|
|
FailureReason))
|
|
{
|
|
SetStatus(
|
|
TEXT("Microphone opened successfully. No test audio was retained."));
|
|
return;
|
|
}
|
|
SetStatus(FailureReason, true);
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleValidateSpeechConfiguration()
|
|
{
|
|
FString FailureReason;
|
|
const bool bCloudProviderSelected =
|
|
Preferences.SpeechProviderId == TEXT("openai")
|
|
|| Preferences.SpeechProviderId == TEXT("groq")
|
|
|| Preferences.SpeechProviderId == TEXT("deepgram")
|
|
|| Preferences.SpeechProviderId == TEXT("elevenlabs");
|
|
if (bCloudProviderSelected && !Preferences.bAllowCloudProviders)
|
|
{
|
|
SetStatus(
|
|
TEXT("Enable cloud providers before using the selected transcription route."),
|
|
true);
|
|
return;
|
|
}
|
|
if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.SpeechEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason))
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
return;
|
|
}
|
|
if (Preferences.SpeechModel.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Choose a transcription model."), true);
|
|
return;
|
|
}
|
|
const bool bCloudCredentialRequired =
|
|
Preferences.SpeechProviderId == TEXT("openai")
|
|
|| Preferences.SpeechProviderId == TEXT("groq")
|
|
|| Preferences.SpeechProviderId == TEXT("deepgram")
|
|
|| Preferences.SpeechProviderId == TEXT("elevenlabs");
|
|
if (bCloudCredentialRequired
|
|
&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("speech-api-key")))
|
|
{
|
|
SetStatus(TEXT("Save a protected key for the selected transcription provider."), true);
|
|
return;
|
|
}
|
|
SetStatus(
|
|
TEXT("Speech configuration is complete. Service reachability is checked when dictation starts."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleValidateVoiceConfiguration()
|
|
{
|
|
FString FailureReason;
|
|
const bool bCloudProviderSelected =
|
|
Preferences.VoiceProviderId == TEXT("openai")
|
|
|| Preferences.VoiceProviderId == TEXT("elevenlabs");
|
|
if (bCloudProviderSelected && !Preferences.bAllowCloudProviders)
|
|
{
|
|
SetStatus(
|
|
TEXT("Enable cloud providers before using the selected voice route."),
|
|
true);
|
|
return;
|
|
}
|
|
if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.VoiceEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason))
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
return;
|
|
}
|
|
if (Preferences.VoiceModel.IsEmpty() || Preferences.VoiceId.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Choose both a voice model and voice identifier."), true);
|
|
return;
|
|
}
|
|
const bool bCloudCredentialRequired =
|
|
Preferences.VoiceProviderId == TEXT("openai")
|
|
|| Preferences.VoiceProviderId == TEXT("elevenlabs");
|
|
if (bCloudCredentialRequired
|
|
&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("voice-api-key")))
|
|
{
|
|
SetStatus(TEXT("Save a protected key for the selected voice provider."), true);
|
|
return;
|
|
}
|
|
SetStatus(
|
|
TEXT("Voice configuration is complete. Service reachability is checked before narration."));
|
|
}
|
|
|
|
void UHyperTwistSettingsPanelWidget::HandleValidateCoachConfiguration()
|
|
{
|
|
if (Preferences.CoachProviderId == TEXT("local-disabled"))
|
|
{
|
|
SetStatus(TEXT("The private built-in guide is ready; no AI service is configured."));
|
|
return;
|
|
}
|
|
|
|
FString FailureReason;
|
|
if (!UHyperTwistPlayerSettingsLibrary::IsProviderEndpointAllowed(
|
|
Preferences.CoachEndpoint,
|
|
Preferences.bAllowCloudProviders,
|
|
FailureReason))
|
|
{
|
|
SetStatus(FailureReason, true);
|
|
return;
|
|
}
|
|
if (Preferences.CoachModel.IsEmpty())
|
|
{
|
|
SetStatus(TEXT("Choose a coach model before enabling AI responses."), true);
|
|
return;
|
|
}
|
|
const bool bCloudCredentialRequired =
|
|
Preferences.CoachProviderId == TEXT("openai")
|
|
|| Preferences.CoachProviderId == TEXT("groq");
|
|
if (bCloudCredentialRequired
|
|
&& !UHyperTwistPlayerSettingsLibrary::HasProviderCredential(
|
|
TEXT("coach-api-key")))
|
|
{
|
|
SetStatus(TEXT("Save a protected key for the selected coach provider."), true);
|
|
return;
|
|
}
|
|
SetStatus(
|
|
TEXT("Coach configuration is complete. The fixed assistant will verify the service on its first request."));
|
|
}
|