Godot4 フォントサイズを調整するエディタ拡張に Undo,Redoの実装

無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、 EditorPlugin クラスを用いて追加したツールメニューアイテムで、独自のエディタ拡張機能を実行した後に、EditorUndoRedoManager を用いて Undo, Redo を実装する第二弾として前回使わなかった、任意のオブジェクトのメンバ関数手続きとして登録する実装例を紹介します。

※ GodotEngine 4.3 を使用しています。.NET 版ではありません。
※スクリプトは自己責任でご使用ください。

前回の記事

前回は、選択中のノード(1番目)の名前Piyo に変更するエディタ拡張の Undo, Redo を、プロパティを設定する手続きを登録する add_do_property, add_undo_property 関数を用いて実装しました。

スクリプト

以下の3個のスクリプトで、Undo, Redo の機能を追加した、プラグイン(ツールメニューから呼び出す形式)を実現します。

例として用いるプラグインは、選択したノード(下位を含む)のフォントサイズを調整します。
フォントサイズを調整する実装例については、以下の記事を参照してください。

以下は、プラグイン用のスクリプトです。
ツールメニューを追加して、そのメニューが呼ばれた際に呼び出す _on_menu_item_sc_fit_font_size 関数を設定しています。
_on_menu_item_sc_fit_font_size 関数は、以前に紹介した選択中のノード(下位を含む)のフォントサイズを調整する実装に、 Undo, Redo を対応させました。

Undo, Redo (実行を含む) の実装は、ハイライトされている行を参照してください。
メンバ関数の呼び出しの手続きを登録できる EditorUndoRedoManageradd_do_method, add_undo_methodo メンバ関数を呼び出します。
Undo, Redo (実行を含む)で実行してもらうメンバ関数は、引数に「任意のオブジェクト」と「そのメンバ関数名」、そのあとに「そのメンバ関数に渡す引数に応じた可変長引数」を渡して登録します。

@tool
extends EditorPlugin
## 選択されているノード群にテキストを持ったコントロールがある場合に、
## コントロールのサイズからはみ出ない最大のサイズにフィットしたフォントサイズに自動調節するエディタ拡張です。
## メニュー「プロジェクト」→「ツール」に追加されたメニューアイテムを選択することで実行されます。

## メニューアイテムの表示名です。
var tool_menu_item_name: StringName = "Sc Fit Font Size"

## プラグインの初期化処理を定義します。
## プラグイン (プロジェクト設定ダイアログ>プラグイン>有効) を有効にした直後や、プラグインが有効な状態のプロジェクトを開いた際に呼び出されます。
func _enter_tree():
	# Initialization of the plugin goes here.
	# メニュー「プロジェクト」→「ツール」にサブメニューを追加します。メニュー選択時に、第二引数のメンバ関数を呼び出します。
	add_tool_menu_item(tool_menu_item_name, Callable(self, "_on_menu_item_sc_fit_font_size"))
	print("sc_fit_font_size.gd: _enter_tree called")

## プラグインの後片付けの処理を定義します。
## プラグインを無効 (プロジェクト設定ダイアログ>プラグイン>有効) にした直後や、プラグインが有効な状態のプロジェクトを閉じた際に呼び出されます。
func _exit_tree():
	# Clean-up of the plugin goes here.
	# 
	remove_tool_menu_item(tool_menu_item_name)
	print("sc_fit_font_size.gd: _exit_tree called")

## メニュー「プロジェクト」→「ツール」→「Sc Fit Font Size」メニューが選択された際に呼び出される関数です。
## 選択中のノード群またはその下位のノード群の Control 派生クラスの
## 表示テキストがコントロールのサイズに合わさるようにフォントサイズを調整します。
func _on_menu_item_sc_fit_font_size():
	## Undo, Redo 操作を実装するためのオブジェクトです。
	var editor_undo_redo_manager: EditorUndoRedoManager = get_undo_redo()

	# アクションを登録します。
	editor_undo_redo_manager.create_action(tool_menu_item_name)
	
	# 選択中のノード群(下位を含む)で表示テキストを持つ Control 派生クラスのフォントサイズを最適な値に変更します。
	
	# 引数と同等と思われる配列を取得します。詳細は呼び出している関数の説明を参照してください。
	var selected_nodes: Array[Node] = ScUtilEditor.get_editor_plugin_handles_argumnt(self)
	# 選択されているノード群とその下位ノード群の Control 派生ノードを探します。
	var found_nodes: Array[Node] = []
	for selected_node in selected_nodes:
		ScUtil.find_nodes_of_specified_class(selected_node, "Control", found_nodes)
	# 配列の要素を重複しないように変換します。
	var found_unique_nodes: Array = ScUtil.to_unique_array(found_nodes)
	
	# 探した Control 派生クラスのテキストのフォントサイズを調整します。
	for node in found_unique_nodes:
		var control: Control = node as Control
		if control != null:
			#ScUtil.fit_control_font_size(control, ScUtil.get_control_text(control))

			# Thmee Overrides のフォントサイズのプロパティ名をクラスから判別します。
			var font_size_property_name: String = "font_size"
			if control is RichTextLabel:
				font_size_property_name = "normal_font_size"
			
			# Undo するために、変更前の Theme Overrides のフォントサイズの情報を取得します。
			var old_font_size = null
			if control.has_theme_font_size_override(font_size_property_name) == true:
				old_font_size = control.get_theme_font_size("theme_override_font_sizes/" + font_size_property_name)
			# 最適なフォントサイズを求めます。この値で Theme Overrides のフォントサイズを上書きします。
			var new_font_size: int = ScUtil.calculate_control_font_size_to_fit(control, ScUtil.get_control_text(control))
			# 変更するフォントサイズが得られた場合は、そのコントロールのフォントサイズを変更します。
			# 変更する際のみ redo, undo のコマンドを追加します。
			if new_font_size > 0:
					
				# 実行または redo で行う関数呼び出しのコマンドを追加します。
				editor_undo_redo_manager.add_do_method(control, "add_theme_font_size_override", font_size_property_name, new_font_size)
				
				# Undo のコマンドを登録します。
				if old_font_size != null:
					# そのフォントサイズが設定されている場合は、その値に戻すための関数呼び出しを Undo で行うようにコマンドを追加します。
					editor_undo_redo_manager.add_undo_method(control, "add_theme_font_size_override", font_size_property_name, old_font_size)
				else:
					# そのフォントサイズが設定されていない場合は、その設定を削除する関数呼び出しを Undo で行うようにコマンドを追加します。
					editor_undo_redo_manager.add_undo_method(control, "remove_theme_font_size_override", font_size_property_name)
		else:
			push_error("_on_menu_item_sc_fit_font_size: Control 派生を探した結果に Control を派生していない Node がありました。")
	
	# add_do_* 系の関数を全て実行します。
	# あとで Undo の指示がきた場合は、add_undo_* 系の関数で設定した
	# プロパティ値の変更、メンバ関数呼び出しなどを行います。
	editor_undo_redo_manager.commit_action()
	return

以下は、前述のスクリプトから呼び出しているランタイムでも利用できるユーティリティ関数です。
※以前に紹介した関数と同じ名前ですが、サポート対象のコントロールを判別するなどの機能を追加しています。

extends Object
class_name ScUtil
## SakuraCrowd が作成したユーティリティ関数群です。
##
## 自作の static 関数群です。行数が増えたら、機能ごとに分けるかもしれません。
## 本スクリプトは自己責任でご使用ください。

## [param array] の要素から重複を除いた配列に変換した結果を返します。
## 辞書型のキーは一意な特性を利用して、配列の要素から重複を除いたユニークな配列を作成します。
## 例えば ["A", "B", "C", "C", "C", "A"] の場合 ["A", "B", "C"] に変換します。
static func to_unique_array(array: Array) -> Array:
	var dictionary: Dictionary = {}
	for element in array:
		# 配列の要素をキーとして、値を設定します。これにより重複しないキーに対応する値が割り当てられます。
		# 値に特に意味はありません。
		dictionary[element] = true
	# 一意のキーの配列を返します。
	return dictionary.keys()

## [param name_of_class] はクラス名の文字列を指定します。
## [param found_nodes] は、条件に合ったノードを追加する配列です。
## 指定したノードまたはその下位のノード群の中に1つでも指定したクラス(またはその派生)のノードがあれば true を、他は false を返します。
static func find_nodes_of_specified_class(node: Node, name_of_class: String, found_nodes: Array[Node]) -> bool:
	var is_one_or_more_matches: bool = false # 戻り値
	if node.is_class(name_of_class):
		#print(name_of_class, " Node found. node = ", node)
		found_nodes.append(node)
		is_one_or_more_matches = true
	for lower_node in node.get_children():
		if find_nodes_of_specified_class(lower_node, name_of_class, found_nodes) == true:
			is_one_or_more_matches = true
	return is_one_or_more_matches



## [param control] のサイズにフィットするように、 [param text] を描画できる最適なフォントサイズを設定します。
## [param text] が空文字列の場合は何もしません。
## フォントサイズは、 [param control] の Theme Overrides の Font Size で設定します。	
static func fit_control_font_size(control: Control, text: String) -> void:
	var font_size: int = calculate_control_font_size_to_fit(control, text)
	# コントロールからはみでないフォントサイズを設定します。
	# すでに Theme Overrides で設定されている Font Size は上書きされます。
	if font_size > 0:
		if control is RichTextLabel:
			control.add_theme_font_size_override("normal_font_size", font_size)
		else:
			control.add_theme_font_size_override("font_size", font_size)
	return

## [param control] のサイズにフィットするように、 [param text] を描画できる最適なフォントサイズを計算します。
## [param text] が空文字列の場合など、フォントサイズの計算ができなかった場合は 0 を返します。
## [method ScUtil.get_control_content_box_size] でサポートしていないコントロールは対象外です。
static func calculate_control_font_size_to_fit(control: Control, text: String) -> int:
	# 以下のクラスは現在サポート外です。
	if control is RichTextLabel or control is LineEdit or control is TextEdit:
		push_warning(control.get_class() + " は、 Sc Fit Font Size のサポート外です。")
		return 0
		
	# 表示するテキストが空文字列の場合は調整できないので何もしません。
	if text.length() == 0:
		push_warning(str(control) + " にはテキストが入力されていないので調節できません。")
		return 0
	
	# コントロールのサイズを取得します。
	var control_size: Vector2 = get_control_content_box_size(control)
	
	# コントロールの用いるフォントを取得します。
	var font: Font = get_control_font(control)
	
	# 描画サイズを測定するために、テキストを描画するオブジェクトを生成します。
	# RefCounted 派生なので消去は自動的に行われます。
	var text_paragraph: TextParagraph = TextParagraph.new()
	
	# フォントサイズを1ずつ増やしていき、コントロールのサイズからはみ出ない最大のフォントサイズを探します。
	var font_size = 1 # 初期フォントサイズ
	while true:
		# 指定したフォント・フォントサイズ・テキストを設定して、その描画サイズを取得します。
		text_paragraph.clear()
		text_paragraph.add_string(text, font, font_size)
		var text_size: Vector2 = text_paragraph.get_size()
		# テキストの描画サイズがコントロールのサイズ(横または縦)を超えた場合、その直前のフォントサイズで調整します。
		if control_size.x < text_size.x or control_size.y < text_size.y:
			break
		# テキストの描画サイズがコントロールのサイズを超えない場合、さらに 1 増やしたフォントサイズで確認を繰り返します。
		font_size += 1
		
	return font_size

## まだ調査中です。現状は control.size と同じ値を返します。
## control のサイズから border, padding 幅を除いた content_box のサイズを得ます。
## Theme の Constants で定義されている場合にその幅を引きます。
## margin, border, padding, content は W3C Box Model を参照してください。
static func get_control_content_box_size(control: Control) -> Vector2:
	# Control のサイズを取得します。
	var content_box_size: Vector2 = control.size
	#if control is RichTextLabel:
		## 水平、垂直の左右・上下のそれぞれの padding 幅を取得します。両側同じ幅になります。
		#var text_highlight_h_padding: int = control.has_theme_constant("text_highlight_h_padding")
		#var text_highlight_v_padding: int = control.has_theme_constant("text_highlight_v_padding")
		#content_box_size.x -= text_highlight_h_padding * 2
		#content_box_size.y -= text_highlight_v_padding * 2
	#if control is LineEdit:
		#pass
	#if control is TextEdit:
		#pass
	
	return content_box_size
	

## [param control] が用いるフォントを取得します。
static func get_control_font(control: Control) -> Font:
	# control が使用しているフォントを取得します。

	# 各 Control 派生クラスのフォントのプロパティ名を取得します。
	var font_property_name: String = "font"
	if control is RichTextLabel:
		font_property_name = "normal_font"
	
	# theme から取得する関数の引数には、テーマリソースのフォントに関する項目名 "font" (snake_case)を指定します。
	# 参照:https://docs.godotengine.org/ja/4.x/classes/class_control.html#class-control-method-get-theme-color
	var font: Font = null
	if control.has_theme_font_override("font_property_name") or control.has_theme_font("font_property_name") == true:
		# テーマの font 項目または Control クラス派生のノードが持つ Theme Overrides の font 項目が設定されている場合
		# テーマから font 項目の値(Font)を取得します。
		font = control.get_theme_font("font_property_name") 
	else:
		# フォントが設定されていない場合
		# テーマのデフォルトの font 項目の値(Font)を取得します。
		font = control.get_theme_default_font()
	return font

## [param control] の表示テキストの文字列を返します。
## また、[Control] 以下のクラスを継承した、独自の派生クラスも String 型の text プロパティを持たせることで
## 本関数で表示テキストを取得できます。
##
## 現状は [method ScUtil.get_control_text_from_text_property] と同じです。
## しかし text プロパティ以外から表示テキストを得るクラスが追加された場合は、
## 本関数で、クラス判別、クラス名判別を用いて、拡張する予定です。
## そのため、表示テキストを得る場合は、本関数を使用してください。
##
## Godot 4.3 の Control クラスの派生クラスを確認すると、表示テキストは全て String 型の text プロパティでした。
## 該当クラスは Button, TextEdit, LinkButton, Label, LineEdit, RichTextLabel とその派生クラスです。
## https://docs.godotengine.org/ja/4.3/classes/class_control.html#class-control
static func get_control_text(control: Control) -> String:
	return get_control_text_from_text_property(control)

## [Control] 派生のクラスが text プロパティを持っている場合、その text 文字列を返します。
## これは [Control] 派生の表示するテキストがあるクラスが text という名前のプロパティに設定している特徴を利用するものです。
## また、公式ではない独自の [Control] 派生クラスにも対応する方法としても使えます。
##
## [method ScUtil.get_control_text] は、現状同じ処理ですが、 text プロパティ以外にも対応する関数です。
static func get_control_text_from_text_property(control: Control) -> String:
	#print("control = ", control)
	if control.get("text") != null and control.text is String:
		#print("テキストプロパティを持っています。", control.text)
		# String 型の text プロパティを持っている場合
		return control.text
	#print("テキストプロパティを持っていません。")
	return ""

上記スクリプトの一部の関数の説明は以下の記事を参照してください。

以下は、前述のスクリプトから呼び出しているエディタ拡張用のユーティリティ関数です。

extends Object
class_name ScUtilEditor

## SakuraCrowd が作成した、エディタ上で実行できるユーティリティ関数群です。
## WebGL などにエクスポートした際に、定義されなくなる EditorPlugin クラスなどを用います。
## 本スクリプトは自己責任でご使用ください。

## _handles の引数とおそらく同等のものを別の方法で取得します。
## 注意: _handles の引数は Object クラスですが、ここで得られるのは Node クラスの配列です。
##
## EditorPlugin _handles イベント関数で渡される引数は、複数のノードなどを選択している場合 MultiNodeEdit と呼ばれる
## アクセスできないクラスのオブジェクトが渡されます。(Godot4.3 2025/02/20 現在)
## その対処法として、別の方法で現在選択されている object 群を取得して、その配列を返します。
## 参照: Expose MultiNodeEdit as a scriptable class · Issue #8067 · godotengine/godot-proposals - https://github.com/godotengine/godot-proposals/issues/8067
static func get_editor_plugin_handles_argumnt(editor_plugin: EditorPlugin) -> Array[Node]:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	var editor_selection: EditorSelection = editor_interface.get_selection()
	var selected_nodes: Array[Node] = editor_selection.get_selected_nodes()
	return selected_nodes

EditorSelection を取得して、get_selected_nodes メンバ関数から、エディタで選択中のノード群を取得しています。

テスト

前述のプラグインなどのスクリプトを上書きしたら、ツールメニューを呼び出してフォントサイズを調整するエディタ拡張機能実行し、 Undo, Redo でそのアクションの前の状態に戻ることと、再び同じことが実行されることを確認しましょう。

ノードを1個選択(例では Label)して、メニュー「プロジェクト」→「ツール」→「Sc Fit Font Size」(登録したツールメニュー)を選択して、先ほど Undo, Redo などを実装した関数を呼び出します。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例1

以前の記事で紹介した通り、選択しているノード(下位を含む)のコントロールのサイズを超えない範囲でテキストを最大のサイズで表示するようにフォントサイズを調整しました。

EditorUndoRedoManager を用いるようになって、 create_action 関数で指定したアクション名が出力ボトムパネルに表示されるようになりました。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例2

Ctrl + Z キーUndo を指示すると、フォントサイズがツールメニューを実行する前の状態に戻りました。

これは、対象ノードの変更前のフォントサイズの値を設定する Control のメンバ関数を add_undo_method で登録したためです。
Undo 時は、 EditorRedoUndoManager の add_undo_* 系で登録した手続きが順次行われ、実行前の状態に戻します

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例3

Ctrl + Y キーRedo(やり直し)を指示すると、 Undo で戻した状態から再び実行した状態にやり直されます。
例では Undo で実行前の小さめのフォントサイズに戻ったあとの Redo で最初に調整した大きめのフォントサイズへの変更処理が再び実行されました。

これは、実行時と同じく EditorRedoUndoManager の add_do_* 系の関数で登録した手続きを順次行い、アクションを実行します。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例4

今まで述べてきた、実行、 Undo、Redo、は複数ノードへアクションを行った場合も同様です。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例5

複数ノードを選択してツールメニューを選び、アクションを実行した後、 Undo を指示すれば、対象の複数ノードの状態実行前に戻ります。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例6

複数ノードを選択してツールメニューを選び、アクションを実行のあと Undo で元の状態に戻した後、 Redo を指示すれば、対象の複数ノードの状態再び実行された後の状態になります。

Godot4.3 EditorUndoRedoManager によるツールメニュー実行アクションの UndoRedo の実装例7

出力ボトムパネルを見ると、ツールメニュー実行の際に create_action で指定したアクション名が出力され、そのあと Undo (Ctrl + Z) で「シーン元に戻す: アクション名」、 Redo (Ctrl + Y) で「シーンやり直す: アクション名」と出力されました。
※こちらでは print 文などをいれていないので、 EditorUndoRedoManager が出力していると思います。

Sc Fit Font Size
シーン元に戻す: Sc Fit Font Size
シーンやり直す: Sc Fit Font Size
シーン元に戻す: Sc Fit Font Size
シーンやり直す: Sc Fit Font Size
シーン元に戻す: Sc Fit Font Size
シーンやり直す: Sc Fit Font Size
Sc Fit Font Size
シーン元に戻す: Sc Fit Font Size
シーンやり直す: Sc Fit Font Size
シーン元に戻す: Sc Fit Font Size
シーンやり直す: Sc Fit Font Size

以下はテスト結果の動画です。

まとめ

今回は、無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 で、 EditorPlugin クラスを用いて追加したツールメニューアイテムで、独自のエディタ拡張機能を実行した後に、EditorUndoRedoManager を用いて Undo, Redo を実装する第二弾として前回使わなかった、任意のオブジェクトのメンバ関数手続きとして登録する実装例を紹介しました。

参照サイト Thank You!

記事一覧 → Compota-Soft-Press

コメント

Ads Blocker Image Powered by Code Help Pro

お願い - Ads Blocker Detected

このサイトは広告を掲載して運営しています。

ポップアップを閉じて閲覧できますが、よろしければ

このサイト内の広告を非表示にする拡張機能をオフにしていただけませんか?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

タイトルとURLをコピーしました