Slateのテキストに無理やり(？)アクセスする方法

#はじめに
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">エディタ上のテキストを翻訳したり、選択中のテキストを翻訳結果に置き換えたりするプラグインをマーケットプレイスに出品しました！<a href="https://t.co/qg57B3J9L5">https://t.co/qg57B3J9L5</a><a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> <a href="https://twitter.com/hashtag/UnrealEngine?src=hash&amp;ref_src=twsrc%5Etfw">#UnrealEngine</a> <a href="https://t.co/EREkC3GWYV">pic.twitter.com/EREkC3GWYV</a></p>&mdash; Naotsun (@Naotsun_UE) <a href="https://twitter.com/Naotsun_UE/status/1457203778713653249?ref_src=twsrc%5Etfw">November 7, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

こちらの翻訳プラグインで実装した機能の中からエディタ上のテキストに無理やり(？)アクセスする方法をご紹介します。
こちらのプラグインでは、マウスポインタ直下にあるテキストを翻訳する機能や、編集中のテキストを翻訳して置き換える機能、ツールチップのテキストを翻訳して置き換える機能があり、それらの翻訳部分を除いた部分の実装方法を見ていきます。

今回の記事で使用するプロジェクトはUE4.27ですが、UE5でも同様に動作します。

#やり方

###Slateのウィジェットクラスのキャスト

まずこれらの機能を作るにあたって去年のアドカレで書いたこちらの記事の内容を使います。
[UnrealC++でSlateのクラスをキャストする](https://qiita.com/Naotsun/items/47945fc303b986571063)

今回のサンプルプロジェクトでは専用のヘッダーを用意しました。
[CastSlateWidget.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/master/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/CastSlateWidget.h)

```CastSlateWidget.h
namespace SamplePlugin
{
	namespace Private
	{
		/**
		 * Cast function for classes that inherit from SWidget.
		 */
		template<class To, class From>
		TSharedPtr<To> CastSlateWidget(TSharedPtr<From> FromPtr, const FName& ToClassName)
		{
			static_assert(TIsDerivedFrom<From, SWidget>::IsDerived, "This implementation wasn't tested for a filter that isn't a child of SWidget.");
			static_assert(TIsDerivedFrom<To, SWidget>::IsDerived, "This implementation wasn't tested for a filter that isn't a child of SWidget.");

			if (FromPtr.IsValid())
			{
				if (FromPtr->GetType() == ToClassName)
				{
					return StaticCastSharedPtr<To>(FromPtr);
				}
			}

			return nullptr;
		}
	}
}

#define CAST_SLATE_WIDGET(ToClass, FromPtr) SamplePlugin::Private::CastSlateWidget<ToClass>(FromPtr, #ToClass)
```

###テキストにアクセスする仕組み

今回、読み取り専用のテキストと編集可能なテキスト、ツールチップのテキストのアクセス方法についてご紹介しますが、いずれも仕組みは同じでアクセスに使用するインターフェースで定義されている関数が違うだけです。
読み取り専用のテキストを例としてテキストにアクセスする仕組みをご紹介します。

[ReadOnlyTextAccessor.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/ReadOnlyTextAccessor.h#L14)

```ReadOnlyTextAccessor.h
	/**
	 * Accessor interface for read-only text widgets.
	 */
	class SAMPLEPLUGIN_API IReadOnlyTextAccessor
	{
	public:
		// Destructor.
		virtual ~IReadOnlyTextAccessor() = default;
		
		// Returns the string set in the widget.
		virtual FText GetText() const = 0;

		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};
```

こちらが読み取り専用のテキストにアクセスするためのインターフェースで、テキストと対象のウィジェットを取得する関数のみが定義されています。

[ReadOnlyTextAccessor.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/ReadOnlyTextAccessor.h#L66)

```ReadOnlyTextAccessor.h
	// The process of converting the passed widget to IReadOnlyTextAccessor.
	DECLARE_DELEGATE_RetVal_OneParam(TSharedPtr<IReadOnlyTextAccessor>, FOnGetReadOnlyTextAccessor, TSharedPtr<SWidget> /* InWidget */);

	/**
	 * Factory class for read-only text widgets accessor.
	 */
	class SAMPLEPLUGIN_API FReadOnlyTextAccessorFactory
	{
	public:
		// Register the widget type name and accessor generation function.
		static void RegisterAccessor(const FName& WidgetTypeName, const FOnGetReadOnlyTextAccessor& Generator);

		// Unregister the widget type name and accessor generation function.
		static void UnregisterAccessor(const FName& WidgetTypeName);

		// Returns whether the specified widget type is registered.
		static bool IsAccessorRegistered(const FName& WidgetTypeName);

		// Create a accessor according to the registered widget type.
		static TSharedPtr<IReadOnlyTextAccessor> CreateAccessor(TSharedPtr<SWidget> InWidget);

	private:
		// Registry of widget type name and accessor generation function.
		static TMap<FName, FOnGetReadOnlyTextAccessor> ReadOnlyTextAccessorRegistry;
	};
```

次にこちらがウィジェットのクラス名と上のインターフェースを実装した各ウィジェット用のクラスの生成関数を登録するクラスです。

[ReadOnlyTextAccessor.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/ReadOnlyTextAccessor.cpp#L32)

```ReadOnlyTextAccessor.cpp
	TSharedPtr<IReadOnlyTextAccessor> FReadOnlyTextAccessorFactory::CreateAccessor(TSharedPtr<SWidget> InWidget)
	{
		if (InWidget.IsValid())
		{
			if (const FOnGetReadOnlyTextAccessor* Generator = ReadOnlyTextAccessorRegistry.Find(InWidget->GetType()))
			{
				return Generator->Execute(InWidget);
			}
		}

		return nullptr;
	}
```

`CreateAccessor`の引数で渡されたウィジェットの型が登録されていたら生成関数を実行し`IReadOnlyTextAccessor`を生成します。(登録されてなかったらnullptrを返す)

[ReadOnlyTextAccessor.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/ReadOnlyTextAccessor.h#L33)

```ReadOnlyTextAccessor.h
	/**
	 * Template class used when registering widgets in FReadOnlyTextAccessorFactory.
	 * Only classes with functions that must be overridden in
	 * IReadOnlyTextAccessor can be specified in TWidgetClass.
	 */
	template<class TWidgetClass>
	class TReadOnlyText : public IReadOnlyTextAccessor
	{
	public:
		// Constructor.
		TReadOnlyText(TSharedPtr<TWidgetClass> InReadOnlyText)
			: IReadOnlyTextAccessor()
			, ReadOnlyText(InReadOnlyText)
		{
		}
	
		// IReadOnlyTextWrapper interface.
		virtual FText GetText() const override
		{
			if (ReadOnlyText.IsValid())
			{
				return ReadOnlyText->GetText();
			}

			return FText();
		}

		virtual TSharedPtr<SWidget> AsWidget() const override
		{
			return ReadOnlyText;
		}
		// End of IReadOnlyTextWrapper interface.

	private:
		// An instance of the actual widget.
		TSharedPtr<TWidgetClass> ReadOnlyText;
	};
```

こちらは`IReadOnlyTextAccessor`を実装するクラスの基本的な形です。
特別な実装をする場合以外はこのクラステンプレートを使用する感じになります。

[DefaultSlateTextAccessor.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/DefaultSlateTextAccessor.cpp#L88)

```DefaultSlateTextAccessor.cpp
		FReadOnlyTextAccessorFactory::RegisterAccessor(
			GET_CLASS_NAME(STextBlock),
			FOnGetReadOnlyTextAccessor::CreateLambda(
			[](TSharedPtr<SWidget> InWidget) -> TSharedPtr<IReadOnlyTextAccessor>
			{
				if (TSharedPtr<STextBlock> TextBlock = CAST_SLATE_WIDGET(STextBlock, InWidget))
				{
					return MakeShared<TReadOnlyText<STextBlock>>(TextBlock);
				}

				return nullptr;
			})
		);
```

実際の使用例として`STextBlock`を登録するコードです。
`STextBlock`は先程のクラステンプレートで問題ないので引数で渡されてきたウィジェットのクラスを`STextBlock`にキャストして`TReadOnlyText`のコンストラクタで渡し、`TReadOnlyText`を返してあげます。

[DefaultSlateTextAccessor.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/DefaultSlateTextAccessor.cpp#L54)

```DefaultSlateTextAccessor.cpp
		class FHyperlinkImpl : public IReadOnlyTextAccessor
		{
		public:
			// Constructor.
			FHyperlinkImpl(TSharedPtr<SHyperlink> InHyperlink)
				: IReadOnlyTextAccessor()
				, Hyperlink(InHyperlink)
			{
			}
	
			// IReadOnlyTextAccessor interface.
			virtual FText GetText() const override
			{
				if (Hyperlink.IsValid())
				{
					return Hyperlink->GetAccessibleText();
				}

				return FText();
			}

			virtual TSharedPtr<SWidget> AsWidget() const override
			{
				return Hyperlink;
			}
			// End of IReadOnlyTextAccessor interface.

		private:
			TSharedPtr<SHyperlink> Hyperlink;
		};
```

`SHyperlink`の様に`TReadOnlyText`が使用できない場合はこのようにそのウィジェットクラス用の`IReadOnlyTextAccessor`を実装したクラスを用意する必要があります。

**ざっくり要約すると、Factoryクラスにウィジェットの型名とその型用の共通のインターフェースを実装したクラスを生成する関数を登録して、生成関数に`TSharedPtr<SWidget>`を渡した時に登録されている型とマッチするのであればその型用のクラスを生成して返すという感じです。
インターフェースからテキストの取得やその他の操作を行うことができます。**

一通りの実装の流れを紹介しましたが、後述する編集可能テキストやツールチップのテキストも同様の方法で実装されていますので、以降この辺りの実装についての紹介は省きます。

###マウスポインタ直下にあるテキストにアクセスしてみる

では、ここまでで確認した`IReadOnlyTextAccessor`を使用して本題であるマウスポインタ直下にあるテキストへのアクセスを行う部分を見ていきましょう。

[SamplePluginSlateHelpers.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/Utilities/SamplePluginSlateHelpers.cpp#L18)

```SamplePluginSlateHelpers.cpp
	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor()
	{
		FSlateApplication& SlateApplication = FSlateApplication::Get();
		const FWidgetPath WidgetsUnderCursor = SlateApplication.LocateWindowUnderMouse(
			SlateApplication.GetCursorPos(), SlateApplication.GetInteractiveTopLevelWindows()
		);
 
		if (WidgetsUnderCursor.IsValid())
		{
			const FArrangedChildren& Widgets = WidgetsUnderCursor.Widgets;
			if (Widgets.Num() > 0)
			{
				return Widgets.Last().Widget;
			}
		}
	
		return nullptr;
	}
```

まずはマウスポインタ直下にあるウィジェットを取得する関数を見ていきましょう。
`FSlateApplication`に`LocateWindowUnderMouse`というマウスポインタ直下にあるウィジェットパス(使用中のウィンドウから最下層のウィジェットまでのウィジェット)を返す関数があるので、それを使用して最下層のウィジェットを返すようにしています。
こちらの関数はウィジェット関連のデバッグなどでも便利なので覚えておくといつか助かるかもしれません。

[SamplePluginActions.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/CommandActions/SamplePluginActions.cpp#L15)

```SamplePluginActions.cpp
	void FSamplePluginActions::TestReadOnlyTextAccessor()
	{
		// Find the read only text to process.
		TSharedPtr<IReadOnlyTextAccessor> ReadOnlyText = nullptr;
		{
			const TSharedPtr<SWidget> Widget = FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor();
			if (Widget.IsValid())
			{
				ReadOnlyText = FReadOnlyTextAccessorFactory::CreateAccessor(Widget);
			}
		}

		if (ReadOnlyText.IsValid())
		{
			const FText& Text = ReadOnlyText->GetText();
			if (!Text.IsEmpty())
			{
				UE_LOG(LogSamplePlugin, Log, TEXT("%s"), *Text.ToString());
			}
		}
	}
```

続いて取得したウィジェットからテキストを取り出す部分ですが、上記の関数でマウスポインタ直下にあるウィジェットを取得し、取得したウィジェットを`FReadOnlyTextAccessorFactory::CreateAccessor`に渡して`IReadOnlyTextAccessor`を生成します。
ここで取得したウィジェットが`FReadOnlyTextAccessorFactory`に登録されていたら`IReadOnlyTextAccessor`を通してテキストを取得しログ出力します。

サンプルプロジェクトでは`Shift+Ctrl+Z`でマウスポインタ直下にあるテキストをログ出力する機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインでは[マウスポインタ直下にあるテキストを翻訳し、その結果をポップアップウィンドウで表示する機能](https://github.com/Naotsun19B/TranslationToolkit-Document/blob/main/README_JP.md#translate)で使用されています。

###編集中のテキストにアクセスしてみる

[EditableTextAccessor.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/EditableTextAccessor.h#L14)

```EditableTextAccessor.h
	/**
	 * Accessor interface for editable text widgets.
	 */
	class SAMPLEPLUGIN_API IEditableTextAccessor
	{
	public:
		// Destructor.
		virtual ~IEditableTextAccessor() = default;
		
		// Returns the string set in the widget.
		virtual FText GetText() const = 0;

		// Returns the string selected in the widget.
		virtual FText GetSelectedText() const = 0;
			
		// Sets the text of the widget.
		virtual void SetText(const FText& Text) = 0;

		// Query to see if any text is selected within the widget.
		virtual bool AnyTextSelected() const = 0;
			
		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};
```

こちらが編集中のテキストにアクセスするインターフェースです。
読み取り専用のテキストのインターフェースであった機能に加えて選択しているテキストのみを取得する関数や、テキストを設定する関数が追加されています。

[EditableTextHandle.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/EditableTextHandle.h#L16)

```EditableTextHandle.h
	/**
	 * A utility structure for replacing only selected parts.
	 */
	class SAMPLEPLUGIN_API FEditableTextHandle
	{
	public:
		// Constructor.
		FEditableTextHandle(const TSharedPtr<IEditableTextAccessor>& InEditableText);

		// If the text is selected, only the selected part is returned.
		FString GetText() const;
		
		// If the text is selected, replace only that part.
		void SetText(const FString& InText);

		// Returns whether the editable text on which this data is based is valid.
		bool IsValid() const;
		
		// Returns editable text from which this data is based.
		TSharedPtr<IEditableTextAccessor> GetEditableTextAccessor() const;

		// Returns text at the time this handle was generated.
		FString GetCachedText() const;

		// Returns whether any character was selected.
		bool IsSelectedText() const;
		
		// Returns selected text at the time this handle was generated.
		FString GetCachedSelectedText() const;

	private:
		// The editable text from which this data is based.
		TSharedPtr<IEditableTextAccessor> EditableTextAccessor;

		// The text at the time this handle was generated.
		FString CachedText;

		// Whether any character was selected.
		bool bIsSelectedText;
		
		// The selected text at the time this handle was generated.
		FString CachedSelectedText;

		// Owner of EditableText.
		TSharedPtr<SInlineEditableTextBlock> InlineEditableTextBlock;
	};
```

上記のインターフェースだけだと少し使い勝手が悪い（例えばインターフェースを生成してから非同期で処理が入り、その後選択していた部分だけを置き換える、など...）ので、`FEditableTextHandle`を使います。
子のクラスを生成した時点のテキストを保持していたり、選択中のテキストのみを置き換えたりできるクラスです。

![2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/f5a24620-f1c1-ae2a-4928-f64de7183905.png)
また、このように常に編集モードのテキストならインターフェースを使ってウィジェットにテキストを設定すればテキストの置き換えを行えますが、
![4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/9ad70de8-8406-bea3-6db7-a081fb197c76.png)
変数名や関数名のように編集モードと読み取り専用モードがあるテキストの場合は少し工夫をする必要があります。

[EditableTextHandle.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/EditableTextHandle.cpp#L14)

```EditableTextHandle.cpp
		TSharedPtr<SInlineEditableTextBlock> FindInlineEditableTextBlock(const TSharedPtr<SWidget>& SearchTarget)
		{
			if (!SearchTarget.IsValid())
			{
				return nullptr;
			}

			if (!SearchTarget->IsParentValid())
			{
				return nullptr;
			}

			const TSharedPtr<SWidget> Parent = SearchTarget->GetParentWidget();
			const TSharedPtr<SInlineEditableTextBlock> InlineEditableTextBlock = CAST_SLATE_WIDGET(SInlineEditableTextBlock, Parent);
			if (InlineEditableTextBlock.IsValid())
			{
				return InlineEditableTextBlock;
			}

			return FindInlineEditableTextBlock(Parent);
		}
```

[EditableTextHandle.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/EditableTextHandle.cpp#L56)

```EditableTextHandle.cpp
	FEditableTextHandle::FEditableTextHandle(const TSharedPtr<IEditableTextAccessor>& InEditableText)
		: EditableTextAccessor(InEditableText)
		, bIsSelectedText(false)
	{
		// ~~~

		// When the edit mode and display mode are switched like a comment node or function name,
		// it is necessary to switch the mode from this handle, so the it is cached.
		InlineEditableTextBlock = EditableTextHandleInternal::FindInlineEditableTextBlock(
			EditableTextAccessor->AsWidget()
		);
	}
```

まず、このタイプのテキストブロックは`SInlineEditableTextBlock`が生成した編集用のテキストボックスで読み取り専用モードになると削除されてしまいます。
そのため、親のウィジェットの`SInlineEditableTextBlock`を再帰的に探す関数を使って、コンストラクタでキャッシュしておきます。

[EditableTextHandle.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/EditableTextHandle.cpp#L81)

```EditableTextHandle.cpp
	void FEditableTextHandle::SetText(const FString& InText)
	{
		if (!EditableTextAccessor.IsValid())
		{
			return;
		}

		// Switch to edit mode if there is an InlineEditableTextBlock.
		if (InlineEditableTextBlock.IsValid())
		{
			const TSharedPtr<FSlateUser> SlateUser = FSamplePluginSlateHelpers::GetLocalSlateUser();
			if (SlateUser.IsValid())
			{
				SlateUser->ReleaseAllCapture();
				InlineEditableTextBlock->EnterEditingMode();
			}
		}
		
		if (bIsSelectedText)
		{
			const FString ReplacedString = CachedText.Replace(*CachedSelectedText, *InText);
			EditableTextAccessor->SetText(FText::FromString(ReplacedString));
		}
		else
		{
			EditableTextAccessor->SetText(FText::FromString(InText));
		}
	}
```

`SInlineEditableTextBlock`がある場合は編集モードにしてからテキストの設定を行います。
また、テキストが選択されている場合は選択している部分のみを置き換えて設定しています。

[SamplePluginSlateHelpers.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/Utilities/SamplePluginSlateHelpers.cpp#L37)

```SamplePluginSlateHelpers.cpp
	TSharedPtr<FSlateUser> FSamplePluginSlateHelpers::GetLocalSlateUser()
	{
		return FSlateApplication::Get().GetCursorUser();
	}

	// ~~~

	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetEditingWidget()
	{
		const TSharedPtr<FSlateUser> SlateUser = GetLocalSlateUser();
		if (SlateUser.IsValid())
		{
			return SlateUser->GetFocusedWidget();
		}
	
		return nullptr;
	}
```

続いて編集中のウィジェットの取得の仕方です。
こちらはそこまで難しくなく、`FSlateUser`(GameFrameworkで言うPlayerController的なやつ)からフォーカス中のウィジェットを取得するだけです。

[SamplePluginActions.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Private/CommandActions/SamplePluginActions.cpp#L37)

```SamplePluginActions.cpp
	void FSamplePluginActions::TestEditableTextAccessor()
	{
		// Find the editable text to process.
		TSharedPtr<IEditableTextAccessor> EditableText = nullptr;
		{
			const TSharedPtr<SWidget> Widget = FSamplePluginSlateHelpers::GetEditingWidget();
			if (Widget.IsValid())
			{
				EditableText = FEditableTextAccessorFactory::CreateAccessor(Widget);
			}
		}
	
		if (!EditableText.IsValid())
		{
			return;
		}

		if (EditableText->GetText().IsEmpty() || !EditableText->AnyTextSelected())
		{
			return;
		}

		const TSharedPtr<FEditableTextHandle> EditableTextHandle = MakeShared<FEditableTextHandle>(EditableText);
		check(EditableTextHandle.IsValid());
		
		EditableTextHandle->SetText(TEXT("Test Editable Text Accessor"));
	}
```

こちらが編集中のテキストの選択している部分を`Test Editable Text Accessor`に置き換える部分です。
基本的な流れはマウスポインタ直下にあるテキストにアクセスする場合と同じですが、テキストを設定するために`FEditableTextHandle`を使用しています。

![5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/c09600cc-b4e2-d4e7-b333-a0cc4a33c077.png)
![6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/9ce9bd1f-7a11-4b62-cf9a-f9f16daa396b.png)
![7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/c1553493-2d34-a41c-e80e-13db96f01524.png)

サンプルプロジェクトでは`Shift+Ctrl+X`で編集中のテキストの選択している部分を置き換える機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインでは[編集中のテキストの選択している部分を翻訳結果に置き換える機能](https://github.com/Naotsun19B/TranslationToolkit-Document#translate-and-replace)で使用されています。

###ツールチップのテキストにアクセスしてみる

[TooltipTextAccessor.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/TooltipTextAccessor.h#L14)

```TooltipTextAccessor.h
	/**
	 * Accessor interface for tooltip text widgets.
	 */
	class SAMPLEPLUGIN_API ITooltipTextAccessor
	{
	public:
		// Destructor.
		virtual ~ITooltipTextAccessor() = default;
		
		// Returns the tooltip text set in the widget.
		virtual FText GetTextTooltip() const = 0;

		// Returns the wrapped widget.
		virtual TSharedPtr<SWidget> AsWidget() const = 0;
	};
```

インターフェースは読み取り専用テキストと同じです。

[TooltipTextHandle.h](https://github.com/Naotsun19B/SlateTextAccessor/blob/5526eb0f77f1746db68f01c6ea7ba65b62c33496/Plugins/SamplePlugin/Source/SamplePlugin/Public/SlateTextAccessors/TooltipTextHandle.h#L18)

```TooltipTextHandle.h
	/**
	 * A utility structure for replace widget tooltips until mouse is moved.
	 */
	class SAMPLEPLUGIN_API FTooltipTextHandle
	{
	public:
		// Create this handle and update active instance.
		static TSharedPtr<FTooltipTextHandle> CreateTooltipTextHandle();
		
		// Replace the tooltip of the owner's widget with this handle.
		void SetText(const FString& InText);

		// Returns false if the mouse is already moved or the owner's widget is incorrect.
		bool IsValid() const;

	private:
		// Constructor.
		FTooltipTextHandle();
		
		// Called when the mouse cursor is moved from the widget of the owner of this handle.
		void HandleOnMouseLeave(const FPointerEvent& InEvent);
		
	private:
		// The tooltip owner widget indicated by this handle.
		TSharedPtr<SWidget> TooltipOwner;
		
		// The original tooltip that is returned when the mouse cursor is moved.
		TSharedPtr<IToolTip> OriginalTooltip;
		
		// Whether the mouse cursor has already been moved.
		bool bHasMouseCursorLeft;
		
		// An instance of the currently active handle.
		static TSharedPtr<FTooltipTextHandle> ActiveInstance;
	};
```

インターフェースだけだとテキストの取得しかできないため、ツールチップが消えるまで内容を上書きするために`FTooltipTextHandle`を使います。

[TooltipTextHandle.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/b8c95fff98c0ff25c26f4c713c7d1951856ac20d/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/TooltipTextHandle.cpp#L42)

```TooltipTextHandle.cpp
	FTooltipTextHandle::FTooltipTextHandle()
		: bHasMouseCursorLeft(false)
	{
		TooltipOwner = FSamplePluginSlateHelpers::GetWidgetUnderMouseCursor();
		if (!TooltipOwner.IsValid())
		{
			bHasMouseCursorLeft = true;
			return;
		}
		
		TooltipOwner->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler::CreateRaw(this, &FTooltipTextHandle::HandleOnMouseLeave));
	}

	void FTooltipTextHandle::HandleOnMouseLeave(const FPointerEvent& InEvent)
	{
		if (IsValid())
		{
			TooltipOwner->SetToolTip(OriginalTooltip);
		}
		
		bHasMouseCursorLeft = true;
		TooltipOwner->SetOnMouseLeave(FSimpleNoReplyPointerEventHandler());
		ActiveInstance.Reset();
	}
```

仕組みとしては簡単で、コンストラクタでマウスポインタ直下にあウィジェットをキャッシュし、そのウィジェットからマウスポインタが離れた時のイベントをバインドしていて、マウスポインタが離れた時にキャッシュしておいたオリジナルのツールチップに戻す感じです。

[TooltipTextHandle.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/b8c95fff98c0ff25c26f4c713c7d1951856ac20d/Plugins/SamplePlugin/Source/SamplePlugin/Private/SlateTextAccessors/TooltipTextHandle.cpp#L20)

```TooltipTextHandle.cpp
	void FTooltipTextHandle::SetText(const FString& InText)
	{
		if (!IsValid())
		{
			return;
		}
		
		auto& SlateApplication = FSlateApplication::Get();
		const TSharedPtr<IToolTip> NewTooltip = SlateApplication.MakeToolTip(FText::FromString(InText));
		if (NewTooltip.IsValid())
		{
			OriginalTooltip = TooltipOwner->GetToolTip();
			TooltipOwner->SetToolTip(NewTooltip);
			SlateApplication.UpdateToolTip(true);
		}
	}
```

ツールチップは`IToolTip`を継承したウィジェットを`SWidget`が持っているので、置き換えたいテキストから生成したツールチップを対象の`SWidget`に設定しています。

[SamplePluginSlateHelpers.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/b8c95fff98c0ff25c26f4c713c7d1951856ac20d/Plugins/SamplePlugin/Source/SamplePlugin/Private/Utilities/SamplePluginSlateHelpers.cpp#L66)

```SamplePluginSlateHelpers.cpp
	TSharedPtr<SWidget> FSamplePluginSlateHelpers::GetTooltipWidget()
	{
		// Looking to tooltip window from all visible windows,
		// because we can't get active tooltip window from outside.
		TArray<TSharedRef<SWindow>> VisibleWindows;
		FSlateApplication::Get().GetAllVisibleWindowsOrdered(VisibleWindows);

		for (const auto& VisibleWindow : VisibleWindows)
		{
			// The tooltip window should be the top window with no title.
			const bool bHasNoTitle = VisibleWindow->GetTitle().IsEmpty();
			const bool bIsTopmostWindow = VisibleWindow->IsTopmostWindow();
			if (!bHasNoTitle || !bIsTopmostWindow)
			{
				continue;
			}
				
			const FChildren* Children = VisibleWindow->GetChildren();
			if (Children == nullptr)
			{
				continue;
			}

			for (int32 Index = 0; Index < Children->Num(); Index++)
			{
				const TSharedRef<const SWidget> ChildWidget = Children->GetChildAt(Index);
				const TSharedPtr<SWidget> ChildWidgetPtr = ConstCastSharedRef<SWidget>(ChildWidget);
				const TSharedPtr<SWeakWidget> WeakWidget = CAST_SLATE_WIDGET(SWeakWidget, ChildWidgetPtr);
				if (WeakWidget.IsValid())
				{
					return WeakWidget;
				}
			}
		}
		
		return nullptr;
	}
```

意外にも(？)現在表示しているツールチップを取得する関数がない(`FSlateApplication`のprivate関数はある)ので、全ウィンドウからツールチップで使用しているウィンドウのウィジェットを探す関数を作りました。

[SamplePluginActions.cpp](https://github.com/Naotsun19B/SlateTextAccessor/blob/b8c95fff98c0ff25c26f4c713c7d1951856ac20d/Plugins/SamplePlugin/Source/SamplePlugin/Private/CommandActions/SamplePluginActions.cpp#L66)

```SamplePluginActions.cpp
	void FSamplePluginActions::TestTooltipTextAccessor()
	{
		const TSharedPtr<SWidget> TooltipWidget = FSamplePluginSlateHelpers::GetTooltipWidget();

		// Find the SToolTip contained in the tooltip widget and get the text.
		TSharedPtr<ITooltipTextAccessor> TooltipText = nullptr;
		{
			TArray<TSharedPtr<SWidget>> ChildWidgets;
			FSamplePluginSlateHelpers::GetAllChildWidgets(TooltipWidget, ChildWidgets);
			for (const auto& ChildWidget : ChildWidgets)
			{
				TooltipText = FTooltipTextAccessorFactory::CreateAccessor(ChildWidget);
				if (TooltipText.IsValid())
				{
					break;
				}
			}
		}

		if (TooltipText.IsValid())
		{
			const FText& TextTooltip = TooltipText->GetTextTooltip();
			if (!TextTooltip.IsEmpty())
			{
				UE_LOG(LogSamplePlugin, Log, TEXT("%s"), *TextTooltip.ToString());
				
				const TSharedPtr<FTooltipTextHandle> TooltipTextHandle = FTooltipTextHandle::CreateTooltipTextHandle();
				check(TooltipTextHandle.IsValid());
				TooltipTextHandle->SetText(TEXT("Test Tooltip Text Accessor"));
			}
		}
	}
```

先程の関数でツールチップのウィンドウウィジェットを取得し、その子ウィジェットを走査してツールチップ本体を見つけています。
`FTooltipTextHandle`は`FEditableTextHandle`と同様に作成からテキストの設定まで非同期の処理を挟むことができます。

![8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/cf5f5b37-8021-1500-9a64-2f7f960793e0.png)
![9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325493/b2fcc9e1-6fa5-4727-6386-5a6082ca5687.png)

サンプルプロジェクトでは`Shift+Ctrl+C`でツールチップのテキストをログ出力してから置き換える機能を確認することができます。

また、冒頭でご紹介した翻訳プラグインでは[ツールチップのテキストを翻訳結果に置き換える機能](https://github.com/Naotsun19B/TranslationToolkit-Document#translate-tooltip)で使用されています。

#おわりに
翻訳プラグインの中でも作るのに苦労した部分だったので記事にしようと思ったのですが、思いのほか説明するのが難しくコードを見せながら少し説明する急ぎ足な記事になってしまいました。
そもそもエディタ上のテキストにアクセスする需要がないのでは？という話ですが、珍しいケースではあると思うので、こんなやり方もあるんだなぁくらいに思って貰えたら助かります。(Slateクラスをキャストできればなんでもできるよ！)

この記事で紹介したプロジェクトは以下でダウンロードできます。
https://github.com/Naotsun19B/SlateTextAccessor

明日は、@nana_321321さんの「BlenderのAutoRigを使ってUE4に持っていくまでのお話」です！