If you consider GameSettings as a MVC model designning, we have discussed about the Model in the last 3 posts, from now on, we will focus on the View, which is correspounds to the widgets in Unreal Engine.

1. UGameSettingListEntryBase

UGameSettingListEntryBase inherits from the UCommonUserWidget and implements an interface IUserObjectListEntry, the latter one allows the widget be managed in a list view widget. It is normal for a game setting entry should be organized in a list view panel.

UCLASS(Abstract, NotBlueprintable, meta = (Category = "Settings", DisableNativeTick))
class GAMESETTINGS_API UGameSettingListEntryBase : public UCommonUserWidget, public IUserObjectListEntry
{
...

It is an abstract class, most of its methods are implemented by its child classes.

Note that it inherits from the UCommonUserWidget instead of the UUserWidget, so it requires for the plugin CommonUI, which provides much stronger features than the original widgets, for example, the navigation for gamepads.

The whole declaration of UGameSettingListEntryBase is as followed:

//////////////////////////////////////////////////////////////////////////
// UAthenaChallengeListEntry
//////////////////////////////////////////////////////////////////////////

UCLASS(Abstract, NotBlueprintable, meta = (Category = "Settings", DisableNativeTick))
class GAMESETTINGS_API UGameSettingListEntryBase : public UCommonUserWidget, public IUserObjectListEntry
{
	GENERATED_BODY()

public:
	virtual void SetSetting(UGameSetting* InSetting);
	virtual void SetDisplayNameOverride(const FText& OverrideName);

protected:
	virtual void NativeOnEntryReleased() override;
	virtual void OnSettingChanged();
	virtual void HandleEditConditionChanged(UGameSetting* InSetting);
	virtual void RefreshEditableState(const FGameSettingEditableState& InEditableState);
	
protected:
	// Focus transitioning to subwidgets for the gamepad
	virtual FReply NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent) override;

	UFUNCTION(BlueprintImplementableEvent)
	UWidget* GetPrimaryGamepadFocusWidget();

protected:
	bool bSuspendChangeUpdates = false;

	UPROPERTY()
	TObjectPtr<UGameSetting> Setting;

	FText DisplayNameOverride = FText::GetEmpty();

private:
	void HandleSettingChanged(UGameSetting* InSetting, EGameSettingChangeReason Reason);

	UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UUserWidget> Background;
};

Most of its methods can be understood by their method names, we only focus on some of them.

It has a userwidget called Background, which is marked with a meta specifier BindWidgetOptional

About BlndWidget and BindWidgetOptional

BindWidget and BindWidgetOptional Are used for connecting C++ member to the UMG widgets, it is not mentioned on UPROPERTY() documentation, for more information, you can read the following website.

Connect C++ to UMG Blueprints with BindWidget · ben🌱ui
How to control logic from C++ and configure visuals in Blueprints.
benui.ca

Let’s take a look at the subclasses, they are much detailed implemented, we take UGameSettingListEntry_Discrete as an example, for we have taken UGameSettingValueDiscreteDynamic as an example before.

2. UGameSettingListEntry_Discrete

UGameSettingListEntry_Discrete are used for the settings which are exactly types of UGameSettingValueDiscrete, no matter dynamic or not dynamic.

This class is also an abstract class, and finally implemented blueprint, the following one is an example of UGameSettingListEntry_Discrete:

Note that there is a subwidget called “Background“, it correspond the Background we mentioned in the source code of UGameSettingListEntryBase.

There are also widgets called Button_Decrease and Button_Increase, which we will find soon in the source code of UGameSettingListEntry_Discrete.

By this way, readers can learn about how the BindWidget and BindWidgetOptional works for connect C++ Members and exactly UMG Widget, it is very important in UI development, most of the times we use C++ for logical driven but use UMG for style and animation, so connecting them together is essential.

After taking a look at an example, now we can dive into the source code of UGameSettingListEntry_Discrete, the declaration of it is as followed:

UCLASS(Abstract, Blueprintable, meta = (Category = "Settings", DisableNativeTick))
class GAMESETTINGS_API UGameSettingListEntrySetting_Discrete : public UGameSettingListEntry_Setting
{
	GENERATED_BODY()

public:
	virtual void SetSetting(UGameSetting* InSetting) override;
	
protected:
	virtual void NativeOnInitialized() override;
	virtual void NativeOnEntryReleased() override;

	void HandleOptionDecrease();
	void HandleOptionIncrease();
	void HandleRotatorChangedValue(int32 Value, bool bUserInitiated);

	void Refresh();
	virtual void OnSettingChanged() override;
	virtual void HandleEditConditionChanged(UGameSetting* InSetting) override;
	virtual void RefreshEditableState(const FGameSettingEditableState& InEditableState) override;

protected:
	UPROPERTY()
	TObjectPtr<UGameSettingValueDiscrete> DiscreteSetting;

private:	// Bound Widgets
	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UPanelWidget> Panel_Value;

	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UGameSettingRotator> Rotator_SettingValue;

	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UCommonButtonBase> Button_Decrease;

	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UCommonButtonBase> Button_Increase;
};

We initialzing the widget(in the method NativeOnInitialized()), it will bind the widgets with the delegates, that is why when players press button decrease or button increase there will be something happed:

void UGameSettingListEntrySetting_Discrete::NativeOnInitialized()
{
	Super::NativeOnInitialized();

	Rotator_SettingValue->OnRotatedEvent.AddUObject(this, &ThisClass::HandleRotatorChangedValue);
	Button_Decrease->OnClicked().AddUObject(this, &ThisClass::HandleOptionDecrease);
	Button_Increase->OnClicked().AddUObject(this, &ThisClass::HandleOptionIncrease);
}

The widget Rotatior_SettingValue is of the UGameSettingRotator type, the latter inherits from UCommonRotator, which allows when the player press increase when the widget already set to the max, it will roll back to 0, and to max when player clicked decrease when the widget was set to 0.

The HandleOptionDecrease() and HandleOptionIncrease method finally change the value of the setting now the widget is managing.

void UGameSettingListEntrySetting_Discrete::HandleOptionDecrease()
{
	//TODO NDarnell Doing this through the UI feels wrong, should use Setting directly.
	Rotator_SettingValue->ShiftTextLeft();
	DiscreteSetting->SetDiscreteOptionByIndex(Rotator_SettingValue->GetSelectedIndex());
}

void UGameSettingListEntrySetting_Discrete::HandleOptionIncrease()
{
	//TODO NDarnell Doing this through the UI feels wrong, should use Setting directly.
	Rotator_SettingValue->ShiftTextRight();
	DiscreteSetting->SetDiscreteOptionByIndex(Rotator_SettingValue->GetSelectedIndex());
}

There are also many other UGameSettingListEntry, but we do not need to introduce them in detailed for they are similar with this one, now, we should learn about how to organized them with a list view.

3. GameSettingListView

UGameSettingListView is a list view to organize the settings together, list view styled setting menu is the most generally-used style in game.

The following gives some examples of GameSettingListView, it is easy to implement with GameSetting. Some may not simply display as one column list view(like the image 3), but it is easy to achive by implementing a special GameSettingListView class.

The UGameSettingListView is implemented as:

UCLASS(meta = (EntryClass = GameSettingListEntryBase))
class GAMESETTINGS_API UGameSettingListView : public UListView
{
	GENERATED_BODY()

public:
	UGameSettingListView(const FObjectInitializer& ObjectInitializer);

	void AddNameOverride(const FName& DevName, const FText& OverrideName);

#if WITH_EDITOR
	virtual void ValidateCompiledDefaults(IWidgetCompilerLog& InCompileLog) const override;
#endif

protected:
	virtual UUserWidget& OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf<UUserWidget> DesiredEntryClass, const TSharedRef<STableViewBase>& OwnerTable) override;
	virtual bool OnIsSelectableOrNavigableInternal(UObject* SelectedItem) override;

protected:
	UPROPERTY(EditAnywhere)
	TObjectPtr<UGameSettingVisualData> VisualData;

private:
	TMap<FName, FText> NameOverrides;
};

Node that this class is marked with specifier Entry = GameSettingListEntryBase, which means that it only allows widget of UGameSettingListEntryBase type or its child type to be the members of it.

There is member VisualData with UGameSettingVisualData type. This type is implemented as:

UCLASS(BlueprintType)
class GAMESETTINGS_API UGameSettingVisualData : public UDataAsset
{
	GENERATED_BODY()

public:
	TSubclassOf<UGameSettingListEntryBase> GetEntryForSetting(UGameSetting* InSetting);

	virtual TArray<TSoftClassPtr<UGameSettingDetailExtension>> GatherDetailExtensions(UGameSetting* InSetting);
	
protected:
	virtual TSubclassOf<UGameSettingListEntryBase> GetCustomEntryForSetting(UGameSetting* InSetting);

protected:
	UPROPERTY(EditDefaultsOnly, Category = ListEntries, meta = (AllowAbstract))
	TMap<TSubclassOf<UGameSetting>, TSubclassOf<UGameSettingListEntryBase>> EntryWidgetForClass;

	UPROPERTY(EditDefaultsOnly, Category = ListEntries, meta = (AllowAbstract))
	TMap<FName, TSubclassOf<UGameSettingListEntryBase>> EntryWidgetForName;

	UPROPERTY(EditDefaultsOnly, Category = Extensions, meta = (AllowAbstract))
	TMap<TSubclassOf<UGameSetting>, FGameSettingClassExtensions> ExtensionsForClasses;

	UPROPERTY(EditDefaultsOnly, Category = Extensions)
	TMap<FName, FGameSettingNameExtensions> ExtensionsForName;
};

This class allows developers to set the widgets of a exactly setting. Besides, it also indicate the details of a setting. The following is an example, it describes what the exactly widget is for a setting in Lyra:

We take color blind mode as an example, here we indicate that the setting ColorBlindMode has a detail panel extension called W_ColorBlindExtension, the following images show that how this feature works:

As a result, when generating a widget, UGameSettingListView must to find the proper widget for the given setting, so the method OnGenerateEntryWidgetInternal has been overwritten as the followed:

UUserWidget& UGameSettingListView::OnGenerateEntryWidgetInternal(UObject* Item, TSubclassOf<UUserWidget> DesiredEntryClass, const TSharedRef<STableViewBase>& OwnerTable)
{
	UGameSetting* SettingItem = Cast<UGameSetting>(Item);

	TSubclassOf<UGameSettingListEntryBase> SettingEntryClass = TSubclassOf<UGameSettingListEntryBase>(DesiredEntryClass);
	if (VisualData)
	{
		if (const TSubclassOf<UGameSettingListEntryBase> EntryClassSetting = VisualData->GetEntryForSetting(SettingItem))
		{
			SettingEntryClass = EntryClassSetting;
		}
		else
		{
			//UE_LOG(LogGameSettings, Error, TEXT("UGameSettingListView: No Entry Class Found!"));
		}
	}
	else
	{
		//UE_LOG(LogGameSettings, Error, TEXT("UGameSettingListView: No VisualData Defined!"));
	}

	UGameSettingListEntryBase& EntryWidget = GenerateTypedEntry<UGameSettingListEntryBase>(SettingEntryClass, OwnerTable);

	if (!IsDesignTime())
	{
		if (const FText* Override = NameOverrides.Find(SettingItem->GetDevName()))
		{
			EntryWidget.SetDisplayNameOverride(*Override);
		}

		EntryWidget.SetSetting(SettingItem);
	}

	return EntryWidget;
}

4. UGameSettingPanel

After orginazing the settings with a list, GameSetting then create a panel called UGameSettingPanel in C++, but this one is an abstract class. Compared with the class we have mentioned before in this post, it is a very large class.

A panel contains both the GameSettingListView and GameSettingDetail, it also manages the behaviour of changing the GameSettingDetail when hover or select a setting for another setting.

Note that even through it is marked as an abstract class, all of its methods have been implemented, mark it as abstract just means that it must be inherited from a blueprint class.

The declaration is as followed:

UCLASS(Abstract)
class GAMESETTINGS_API UGameSettingPanel : public UCommonUserWidget
{
	GENERATED_BODY()

public:

	UGameSettingPanel();
	virtual void NativeOnInitialized() override;
	virtual void NativeConstruct() override;
	virtual void NativeDestruct() override;

	// Focus transitioning to subwidgets for the gamepad
	virtual FReply NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent) override;

	/**  */
	void SetRegistry(UGameSettingRegistry* InRegistry);

	/** Sets the filter for this panel, restricting which settings are available currently. */
	void SetFilterState(const FGameSettingFilterState& InFilterState, bool bClearNavigationStack = true);

	/** Gets the currently visible and available settings based on the filter state. */
	TArray<UGameSetting*> GetVisibleSettings() const { return VisibleSettings; }

	/** Can we pop the current navigation stack */
	bool CanPopNavigationStack() const;

	/** Pop the navigation stack */
	void PopNavigationStack();

	/**
	 * Gets the set of settings that are potentially available on this screen.
	 * MAY CONTAIN INVISIBLE SETTINGS.
	 * DOES NOT INCLUDED NESTED PAGES.
	 */
	TArray<UGameSetting*> GetSettingsWeCanResetToDefault() const;

	void SelectSetting(const FName& SettingDevName);
	UGameSetting* GetSelectedSetting() const;

	void RefreshSettingsList();

	FOnFocusedSettingChanged OnFocusedSettingChanged;

protected:
	void RegisterRegistryEvents();
	void UnregisterRegistryEvents();
	
	void HandleSettingItemHoveredChanged(UObject* Item, bool bHovered);
	void HandleSettingItemSelectionChanged(UObject* Item);
	void FillSettingDetails(UGameSetting* InSetting);
	void HandleSettingNamedAction(UGameSetting* Setting, FGameplayTag GameSettings_Action_Tag);
	void HandleSettingNavigation(UGameSetting* Setting);
	void HandleSettingEditConditionsChanged(UGameSetting* Setting);

private:

	UPROPERTY(Transient)
	TObjectPtr<UGameSettingRegistry> Registry;

	UPROPERTY(Transient)
	TArray<TObjectPtr<UGameSetting>> VisibleSettings;

	UPROPERTY(Transient)
	TObjectPtr<UGameSetting> LastHoveredOrSelectedSetting;

	UPROPERTY(Transient)
	FGameSettingFilterState FilterState;

	UPROPERTY(Transient)
	TArray<FGameSettingFilterState> FilterNavigationStack;

	FName DesiredSelectionPostRefresh;

	bool bAdjustListViewPostRefresh = true;

private:	// Bound Widgets
	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UGameSettingListView> ListView_Settings;

	UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UGameSettingDetailView> Details_Settings;

private:
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnExecuteNamedActionBP, UGameSetting*, Setting, FGameplayTag, Action);
	UPROPERTY(BlueprintAssignable, Category = Events, meta = (DisplayName = "On Execute Named Action"))
	FOnExecuteNamedActionBP BP_OnExecuteNamedAction;

private:
	FTSTicker::FDelegateHandle RefreshHandle;
};

It has a member ListView_Settings of UGameSettingListView type and a member of UGameSettingDetailView, the latter class we have not introduced before, but with the content we have discussed, readers can easily learn about what it is now.

When the panel is initializing, it bind two delegates to the list view to achieve the hover event and changing select event:

void UGameSettingPanel::NativeOnInitialized()
{
	Super::NativeOnInitialized();

	ListView_Settings->OnItemIsHoveredChanged().AddUObject(this, &ThisClass::HandleSettingItemHoveredChanged);
	ListView_Settings->OnItemSelectionChanged().AddUObject(this, &ThisClass::HandleSettingItemSelectionChanged);
}

These events are not used for animation, they are used for more exactly logic, such as changing the detail panel:

void UGameSettingPanel::HandleSettingItemHoveredChanged(UObject* Item, bool bHovered)
{
	UGameSetting* Setting = bHovered ? Cast<UGameSetting>(Item) : ToRawPtr(LastHoveredOrSelectedSetting);
	if (bHovered && Setting)
	{
		LastHoveredOrSelectedSetting = Setting;
	}

	FillSettingDetails(Setting);
}

void UGameSettingPanel::HandleSettingItemSelectionChanged(UObject* Item)
{
	UGameSetting* Setting = Cast<UGameSetting>(Item);
	if (Setting)
	{
		LastHoveredOrSelectedSetting = Setting;
	}

	FillSettingDetails(Cast<UGameSetting>(Item));
}

As for the animation, it is implemented by the event OnMouseEnter/OnMouseExit of the ListEntry class.

It also register the event of GameSettingRegistry:

void UGameSettingPanel::RegisterRegistryEvents()
{
	if (Registry)
	{
		Registry->OnSettingEditConditionChangedEvent.AddUObject(this, &ThisClass::HandleSettingEditConditionsChanged);
		Registry->OnSettingNamedActionEvent.AddUObject(this, &ThisClass::HandleSettingNamedAction);
		Registry->OnExecuteNavigationEvent.AddUObject(this, &ThisClass::HandleSettingNavigation);
	}
}

void UGameSettingPanel::UnregisterRegistryEvents()
{
	if (Registry)
	{
		Registry->OnSettingEditConditionChangedEvent.RemoveAll(this);
		Registry->OnSettingNamedActionEvent.RemoveAll(this);
		Registry->OnExecuteNavigationEvent.RemoveAll(this);
	}
}

The other logic of the class are easy to understant.

5. UGameSettingScreen

This will be the last class about the plugin GameSetting, note that UGameSettingPanel only represents a tab for game setting in UGameSettingRegistry, but, as we know, it has Video, Audio, Gameplay and many other widgets.

So we encapsulate an advanced class called UGameSettingScreen, this class is used to represents the whole setting menu page.

UCLASS(Abstract, meta = (Category = "Settings", DisableNativeTick))
class GAMESETTINGS_API UGameSettingScreen : public UCommonActivatableWidget
{
	GENERATED_BODY()
public:

protected:
	virtual void NativeOnInitialized() override;
	virtual void NativeOnActivated() override;
	virtual void NativeOnDeactivated() override;
	virtual UWidget* NativeGetDesiredFocusTarget() const override;

	UFUNCTION(BlueprintCallable)
	void NavigateToSetting(FName SettingDevName);
	
	UFUNCTION(BlueprintCallable)
	void NavigateToSettings(const TArray<FName>& SettingDevNames);

	UFUNCTION(BlueprintNativeEvent)
	void OnSettingsDirtyStateChanged(bool bSettingsDirty);
	virtual void OnSettingsDirtyStateChanged_Implementation(bool bSettingsDirty) { }

	UFUNCTION(BlueprintCallable)
	bool AttemptToPopNavigation();

	UFUNCTION(BlueprintCallable)
	UGameSettingCollection* GetSettingCollection(FName SettingDevName, bool& HasAnySettings); 

protected:
	virtual UGameSettingRegistry* CreateRegistry() PURE_VIRTUAL(, return nullptr;);

	template <typename GameSettingRegistryT = UGameSettingRegistry>
	GameSettingRegistryT* GetRegistry() const { return Cast<GameSettingRegistryT>(const_cast<UGameSettingScreen*>(this)->GetOrCreateRegistry()); }

	UFUNCTION(BlueprintCallable)
	virtual void CancelChanges();

	UFUNCTION(BlueprintCallable)
	virtual void ApplyChanges();

	UFUNCTION(BlueprintCallable)
	bool HaveSettingsBeenChanged() const { return ChangeTracker.HaveSettingsBeenChanged(); }

	void ClearDirtyState();

	void HandleSettingChanged(UGameSetting* Setting, EGameSettingChangeReason Reason);

	FGameSettingRegistryChangeTracker ChangeTracker;

private:
	UGameSettingRegistry* GetOrCreateRegistry();

private:	// Bound Widgets
	UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
	TObjectPtr<UGameSettingPanel> Settings_Panel;

	UPROPERTY(Transient)
	mutable TObjectPtr<UGameSettingRegistry> Registry;
};

Abstract class for sure, it is implemented by blueprint, there is nothing special between it and GameSettingPanel, we do not waste time to introducing on it, you can read it by yourself now.

In the following content, let’s focus on a special feature: how it deals with keyboard binding?

The answer is in the class UGameSettingPressAnyKey:

UCLASS(Abstract)
class GAMESETTINGS_API UGameSettingPressAnyKey : public UCommonActivatableWidget
{
	GENERATED_BODY()

public:
	UGameSettingPressAnyKey(const FObjectInitializer& Initializer);

	DECLARE_EVENT_OneParam(UGameSettingPressAnyKey, FOnKeySelected, FKey);
	FOnKeySelected OnKeySelected;

	DECLARE_EVENT(UGameSettingPressAnyKey, FOnKeySelectionCanceled);
	FOnKeySelectionCanceled OnKeySelectionCanceled;

protected:
	virtual void NativeOnActivated() override;
	virtual void NativeOnDeactivated() override;

	void HandleKeySelected(FKey InKey);
	void HandleKeySelectionCanceled();

	void Dismiss(TFunction<void()> PostDismissCallback);

private:
	bool bKeySelected = false;
	TSharedPtr<class FSettingsPressAnyKeyInputPreProcessor> InputProcessor;
};

When implementing this, it also create a class inherits from the interface IInputProcess:

class FSettingsPressAnyKeyInputPreProcessor : public IInputProcessor
{
public:
	FSettingsPressAnyKeyInputPreProcessor()
	{

	}

	virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override { }

	virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override
	{
		HandleKey(InKeyEvent.GetKey());
		return true;
	}

	virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override
	{
		return true;
	}

	virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
	{
		HandleKey(MouseEvent.GetEffectingButton());
		return true;
	}

	virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
	{
		return true;
	}

	virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
	{
		HandleKey(MouseEvent.GetEffectingButton());
		return true;
	}

	virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) override
	{
		if (InWheelEvent.GetWheelDelta() != 0)
		{
			const FKey Key = InWheelEvent.GetWheelDelta() < 0 ? EKeys::MouseScrollDown : EKeys::MouseScrollUp;
			HandleKey(Key);
		}
		return true;
	}

	DECLARE_MULTICAST_DELEGATE(FSettingsPressAnyKeyInputPreProcessorCanceled);
	FSettingsPressAnyKeyInputPreProcessorCanceled OnKeySelectionCanceled;

	DECLARE_MULTICAST_DELEGATE_OneParam(FSettingsPressAnyKeyInputPreProcessorKeySelected, FKey);
	FSettingsPressAnyKeyInputPreProcessorKeySelected OnKeySelected;

private:
	void HandleKey(const FKey& Key)
	{
		// Cancel this process if it's Escape, Touch, or a gamepad key.
		if (Key == EKeys::LeftCommand || Key == EKeys::RightCommand)
		{
			// Ignore
		}
		else if (Key == EKeys::Escape || Key.IsTouch() || Key.IsGamepadKey())
		{
			OnKeySelectionCanceled.Broadcast();
		}
		else
		{
			OnKeySelected.Broadcast(Key);
		}
	}
};

It broadcast two events, which is binded when the widget is initialzing:

void UGameSettingPressAnyKey::NativeOnActivated()
{
	Super::NativeOnActivated();

	bKeySelected = false;

	InputProcessor = MakeShared<FSettingsPressAnyKeyInputPreProcessor>();
	InputProcessor->OnKeySelected.AddUObject(this, &ThisClass::HandleKeySelected);
	InputProcessor->OnKeySelectionCanceled.AddUObject(this, &ThisClass::HandleKeySelectionCanceled);
	FSlateApplication::Get().RegisterInputPreProcessor(InputProcessor, 0);
}

We take HandleKeySelected as an example, this one triggers and function:

void UGameSettingPressAnyKey::HandleKeySelected(FKey InKey)
{
	if (!bKeySelected)
	{
		bKeySelected = true;
		Dismiss([this, InKey]() {
			OnKeySelected.Broadcast(InKey);
		});
	}
}

What is worth mentioning here is the method Dismiss:

void UGameSettingPressAnyKey::Dismiss(TFunction<void()> PostDismissCallback)
{
	// We delay a tick so that we're done processing input.
	FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateWeakLambda(this, [this, PostDismissCallback](float DeltaTime)
	{
		QUICK_SCOPE_CYCLE_COUNTER(STAT_UGameSettingPressAnyKey_Dismiss);

		FSlateApplication::Get().UnregisterInputPreProcessor(InputProcessor);

		DeactivateWidget();

		PostDismissCallback();

		return false;
	}));
}

It called a latent function, the reason why is that it need to delay a tick to done processing input.

It unregister the input process first and then called PostDismissCallback(), the latter broadcast a delegate, which will finally call the ChangeBind method of the setting entry, in lyra, it is ULyraGameSettingEntrySetting_KeyBoardInput who finally do this.

As for when did it register the process? well, it is until it activate:

void UGameSettingPressAnyKey::NativeOnActivated()
{
	Super::NativeOnActivated();

	bKeySelected = false;

	InputProcessor = MakeShared<FSettingsPressAnyKeyInputPreProcessor>();
	InputProcessor->OnKeySelected.AddUObject(this, &ThisClass::HandleKeySelected);
	InputProcessor->OnKeySelectionCanceled.AddUObject(this, &ThisClass::HandleKeySelectionCanceled);
	FSlateApplication::Get().RegisterInputPreProcessor(InputProcessor, 0);
}

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *