diff --git a/.gitignore b/.gitignore index a5a8cdc8..6b023087 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Project-related -project/addons/ +project/addons/gdUnit4 +project/addons/sentry/* +!project/addons/sentry/user_feedback/ project/export_presets.cfg project/.vscode/ project/reports/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ed83a06e..b77ba342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add user feedback API for collecting and sending user feedback to Sentry ([#418](https://github.com/getsentry/sentry-godot/pull/418)) +- Add customizable User Feedback form that can be used for feedback submission, and as an example for custom implementations ([#422](https://github.com/getsentry/sentry-godot/pull/422)) - Access event exception values in `before_send` handler ([#415](https://github.com/getsentry/sentry-godot/pull/415)) - Add support for Structured Logging ([#409](https://github.com/getsentry/sentry-godot/pull/409)) diff --git a/project/addons/sentry/user_feedback/logo.svg b/project/addons/sentry/user_feedback/logo.svg new file mode 100644 index 00000000..8234a3ab --- /dev/null +++ b/project/addons/sentry/user_feedback/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project/addons/sentry/user_feedback/logo.svg.import b/project/addons/sentry/user_feedback/logo.svg.import new file mode 100644 index 00000000..46238288 --- /dev/null +++ b/project/addons/sentry/user_feedback/logo.svg.import @@ -0,0 +1,18 @@ +[remap] + +importer="svg" +type="DPITexture" +uid="uid://d0o3nt85ac67i" +path="res://.godot/imported/logo.svg-25820d7157ee760c1db8b8ba461e2e2f.dpitex" + +[deps] + +source_file="res://addons/sentry/user_feedback/logo.svg" +dest_files=["res://.godot/imported/logo.svg-25820d7157ee760c1db8b8ba461e2e2f.dpitex"] + +[params] + +base_scale=1.0 +saturation=1.0 +color_map={} +compress=true diff --git a/project/addons/sentry/user_feedback/sentry_theme.tres b/project/addons/sentry/user_feedback/sentry_theme.tres new file mode 100644 index 00000000..5454fefa --- /dev/null +++ b/project/addons/sentry/user_feedback/sentry_theme.tres @@ -0,0 +1,283 @@ +[gd_resource type="Theme" load_steps=15 format=3 uid="uid://bw0anqwp7xj8t"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hffoe"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.954, 0.954, 0.954, 1) +draw_center = false +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.39607844, 0.34901962, 0.77254903, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e23mj"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.49787024, 0.43896988, 0.9101726, 1) +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g6gn8"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.42352942, 0.37254903, 0.78039217, 1) +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_63sqy"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.42352942, 0.37254903, 0.78039217, 1) +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5o1s5"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.90999997, 0.90999997, 0.90999997, 1) +border_color = Color(0.93, 0.93, 0.93, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4sisp"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.954, 0.954, 0.954, 1) +draw_center = false +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.39607844, 0.34901962, 0.77254903, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gix2e"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.9607843, 0.9529412, 0.96862745, 1) +border_color = Color(0, 0, 0, 0.05) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7qvkq"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.96862745, 0.9647059, 0.9764706, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6xjsq"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.83475, 0.83475, 0.83475, 1) +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_sfof1"] +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_right = 8.0 +content_margin_bottom = 8.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pmhvd"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.96862745, 0.9647059, 0.9764706, 1) +draw_center = false +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.42352942, 0.37254903, 0.78039217, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ij1qw"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.96862745, 0.9647059, 0.9764706, 1) +border_width_bottom = 4 +border_color = Color(0.8784314, 0.8627451, 0.8980392, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3wai3"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(0.9607843, 0.9529412, 0.96862745, 1) +border_width_bottom = 4 +border_color = Color(0.9411765, 0.9254902, 0.9529412, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_detail = 5 +anti_aliasing = false + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qfyhw"] +content_margin_left = 12.0 +content_margin_top = 10.0 +content_margin_right = 12.0 +content_margin_bottom = 10.0 +bg_color = Color(1, 1, 1, 1) +border_color = Color(0.98099995, 0.98099995, 0.98099995, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 +corner_detail = 5 +anti_aliasing = false + +[resource] +default_font_size = 20 +ActionButton/base_type = &"Button" +ActionButton/colors/font_color = Color(1, 1, 1, 1) +ActionButton/colors/font_focus_color = Color(1, 1, 1, 1) +ActionButton/colors/font_hover_color = Color(1, 1, 1, 1) +ActionButton/colors/font_pressed_color = Color(0.9607843, 0.9529412, 0.96862745, 1) +ActionButton/styles/focus = SubResource("StyleBoxFlat_hffoe") +ActionButton/styles/hover = SubResource("StyleBoxFlat_e23mj") +ActionButton/styles/normal = SubResource("StyleBoxFlat_g6gn8") +ActionButton/styles/pressed = SubResource("StyleBoxFlat_63sqy") +BoxContainer/constants/separation = 4 +Button/colors/font_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +Button/colors/font_disabled_color = Color(0.44313726, 0.3882353, 0.49411765, 1) +Button/colors/font_focus_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +Button/colors/font_hover_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +Button/colors/font_hover_pressed_color = Color(0.39607844, 0.34901962, 0.77254903, 1) +Button/colors/font_outline_color = Color(0, 0, 0, 0) +Button/colors/font_pressed_color = Color(0.39607844, 0.34901962, 0.77254903, 1) +Button/colors/icon_disabled_color = Color(1, 1, 1, 0.4) +Button/colors/icon_focus_color = Color(1.45, 1.45, 1.45, 1) +Button/colors/icon_hover_color = Color(1.45, 1.45, 1.45, 1) +Button/colors/icon_hover_pressed_color = Color(0.63, 1.75, 3.5, 1) +Button/colors/icon_normal_color = Color(1, 1, 1, 1) +Button/colors/icon_pressed_color = Color(0.63, 1.75, 3.5, 1) +Button/constants/align_to_largest_stylebox = 1 +Button/constants/h_separation = 8 +Button/constants/outline_size = 0 +Button/styles/disabled = SubResource("StyleBoxFlat_5o1s5") +Button/styles/focus = SubResource("StyleBoxFlat_4sisp") +Button/styles/hover = SubResource("StyleBoxFlat_gix2e") +Button/styles/normal = SubResource("StyleBoxFlat_7qvkq") +Button/styles/pressed = SubResource("StyleBoxFlat_6xjsq") +HeaderMedium/colors/font_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +HeaderMedium/font_sizes/font_size = 26 +Label/colors/font_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +Label/colors/font_outline_color = Color(0, 0, 0, 0) +Label/colors/font_shadow_color = Color(0, 0, 0, 0) +Label/constants/line_spacing = 6 +Label/constants/outline_size = 0 +Label/constants/shadow_offset_x = 2 +Label/constants/shadow_offset_y = 2 +Label/constants/shadow_outline_size = 2 +Label/styles/focus = SubResource("StyleBoxFlat_4sisp") +Label/styles/normal = SubResource("StyleBoxEmpty_sfof1") +LineEdit/colors/caret_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +LineEdit/colors/clear_button_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +LineEdit/colors/clear_button_color_pressed = Color(0.39607844, 0.34901962, 0.77254903, 1) +LineEdit/colors/font_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +LineEdit/colors/font_outline_color = Color(0, 0, 0, 0) +LineEdit/colors/font_placeholder_color = Color(0.44313726, 0.3882353, 0.49411765, 1) +LineEdit/colors/font_selected_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +LineEdit/colors/font_uneditable_color = Color(0, 0, 0, 0.65) +LineEdit/colors/selection_color = Color(0.39607844, 0.34901962, 0.77254903, 0.4) +LineEdit/constants/caret_width = 1 +LineEdit/constants/minimum_character_width = 4 +LineEdit/constants/outline_size = 0 +LineEdit/styles/focus = SubResource("StyleBoxFlat_pmhvd") +LineEdit/styles/normal = SubResource("StyleBoxFlat_ij1qw") +LineEdit/styles/read_only = SubResource("StyleBoxFlat_3wai3") +MainBoxContainer/constants/separation = 10 +PanelContainer/styles/panel = SubResource("StyleBoxFlat_qfyhw") +TextEdit/colors/background_color = Color(0, 0, 0, 0) +TextEdit/colors/caret_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +TextEdit/colors/font_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +TextEdit/colors/font_outline_color = Color(0, 0, 0, 0) +TextEdit/colors/font_placeholder_color = Color(0.44313726, 0.3882353, 0.49411765, 1) +TextEdit/colors/font_readonly_color = Color(0, 0, 0, 0.65) +TextEdit/colors/font_selected_color = Color(0.24313726, 0.20392157, 0.27450982, 1) +TextEdit/colors/selection_color = Color(0.39607844, 0.34901962, 0.77254903, 0.4) +TextEdit/constants/caret_width = 1 +TextEdit/constants/line_spacing = 8 +TextEdit/constants/outline_size = 0 +TextEdit/styles/focus = SubResource("StyleBoxFlat_pmhvd") +TextEdit/styles/normal = SubResource("StyleBoxFlat_ij1qw") +TextEdit/styles/read_only = SubResource("StyleBoxFlat_3wai3") diff --git a/project/addons/sentry/user_feedback/user_feedback_form.gd b/project/addons/sentry/user_feedback/user_feedback_form.gd new file mode 100644 index 00000000..1683ff8f --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_form.gd @@ -0,0 +1,106 @@ +extends PanelContainer +## User Feedback Form +## +## A customizable user feedback panel for use with Sentry SDK. +## The feedback form automatically handles message validation, character limits, and +## integrates with SentrySDK for feedback submission. +## +## Usage: +## 1. Copy the folder with this feedback form into your project to customize it. +## 2. Customize the form as needed, you can tweak the theme file to change the looks. +## 3. Integrate the panel into your existing UI hierarchy. +## +## Files: +## - user_feedback_form.tscn: Feedback form for integrating into existing UI. +## - user_feedback_gui.tscn: Ready to use integration example. +## - sentry_theme.tres: Reference UI theme file. + + +## Emitted when feedback is successfully submitted after the user clicks the "Submit" button. +signal feedback_submitted(feedback: SentryFeedback) +## Emitted when feedback is cancelled after the user clicks the "Cancel" button. +signal feedback_cancelled() + +## Whether to display Sentry logo in the top right corner. +@export var logo_visible: bool = true: + set(value): + logo_visible = value + _update_controls() + +## Whether to display name input field. +@export var name_visible: bool = true: + set(value): + name_visible = value + _update_controls() + +## Whether to display email input field. +@export var email_visible: bool = true: + set(value): + email_visible = value + _update_controls() + +## Minimum number of words required in the feedback message before the feedback can be submitted. +@export var minimum_words: int = 2 + + +@onready var _message_edit: TextEdit = %MessageEdit +@onready var _name_edit: LineEdit = %NameEdit +@onready var _email_edit: LineEdit = %EmailEdit +@onready var _submit_button: Button = %SubmitButton +@onready var _character_counter: Label = %CharacterCounter + + +func _ready() -> void: + _update_controls() + _on_message_edit_text_changed() + + +func _update_controls() -> void: + if is_node_ready(): + %Logo.visible = logo_visible + %EmailSection.visible = email_visible + %NameSection.visible = name_visible + + +func _on_submit_button_pressed() -> void: + var feedback := SentryFeedback.new() + feedback.message = _message_edit.text + feedback.name = _name_edit.text + feedback.contact_email = _email_edit.text + + SentrySDK.capture_feedback(feedback) + + feedback_submitted.emit(feedback) + + # Reset feedback message + _message_edit.text = "" + + +func _on_message_edit_text_changed() -> void: + var message: String = _message_edit.text + if message.length() > 4096: + var col: int = _message_edit.get_caret_column() + message = message.substr(0, 4096) + _message_edit.text = message + _message_edit.set_caret_column(col) + _submit_button.disabled = _count_words(message) < minimum_words + _character_counter.text = str(message.length()) + "/4096" + _character_counter.visible = message.length() > 3000 + + +func _count_words(text: String) -> int: + var words: PackedStringArray = text.strip_edges().split(" ", false) + var clean_words: PackedStringArray = [] + for word in words: + if word.strip_edges() != "": + clean_words.append(word) + return clean_words.size() + + +func _on_cancel_button_pressed() -> void: + feedback_cancelled.emit() + + +func _on_visibility_changed() -> void: + if visible and _message_edit: + _message_edit.grab_focus() diff --git a/project/addons/sentry/user_feedback/user_feedback_form.gd.uid b/project/addons/sentry/user_feedback/user_feedback_form.gd.uid new file mode 100644 index 00000000..81c677c9 --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_form.gd.uid @@ -0,0 +1 @@ +uid://dmtkflr7lmh5h diff --git a/project/addons/sentry/user_feedback/user_feedback_form.tscn b/project/addons/sentry/user_feedback/user_feedback_form.tscn new file mode 100644 index 00000000..a6d06efb --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_form.tscn @@ -0,0 +1,120 @@ +[gd_scene load_steps=3 format=3 uid="uid://bdn5fqm81rhy6"] + +[ext_resource type="Script" uid="uid://dmtkflr7lmh5h" path="res://addons/sentry/user_feedback/user_feedback_form.gd" id="1_0vncv"] +[ext_resource type="Texture2D" uid="uid://d0o3nt85ac67i" path="res://addons/sentry/user_feedback/logo.svg" id="2_useou"] + +[node name="UserFeedbackForm" type="PanelContainer"] +offset_right = 376.0 +offset_bottom = 327.0 +script = ExtResource("1_0vncv") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 16 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 16 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 +theme_type_variation = &"MainBoxContainer" + +[node name="Title" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer/Title"] +layout_mode = 2 +theme_type_variation = &"HeaderMedium" +text = "Give Feedback" + +[node name="Logo" type="TextureRect" parent="MarginContainer/VBoxContainer/Title"] +unique_name_in_owner = true +texture_filter = 3 +layout_mode = 2 +size_flags_horizontal = 10 +texture = ExtResource("2_useou") +expand_mode = 3 + +[node name="MessageSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/MessageSection"] +layout_mode = 2 + +[node name="MessageLabel" type="Label" parent="MarginContainer/VBoxContainer/MessageSection/HBoxContainer"] +layout_mode = 2 +text = "Message" + +[node name="CharacterCounter" type="Label" parent="MarginContainer/VBoxContainer/MessageSection/HBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_horizontal = 10 +text = "0/4096" + +[node name="MessageEdit" type="TextEdit" parent="MarginContainer/VBoxContainer/MessageSection"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 72) +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +placeholder_text = "Tell us about your issue" +wrap_mode = 1 +tab_input_mode = false + +[node name="NameSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="NameLabel" type="Label" parent="MarginContainer/VBoxContainer/NameSection"] +layout_mode = 2 +text = "Name" + +[node name="NameEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/NameSection"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Your name (optional)" + +[node name="EmailSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="EmailLabel" type="Label" parent="MarginContainer/VBoxContainer/EmailSection"] +layout_mode = 2 +text = "Email" + +[node name="EmailEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/EmailSection"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +placeholder_text = "Contact email (optional)" + +[node name="Spacer3" type="Control" parent="MarginContainer/VBoxContainer"] +custom_minimum_size = Vector2(0, 6) +layout_mode = 2 + +[node name="Actions" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="CancelButton" type="Button" parent="MarginContainer/VBoxContainer/Actions"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 6 +text = "Cancel" + +[node name="SubmitButton" type="Button" parent="MarginContainer/VBoxContainer/Actions"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +size_flags_horizontal = 6 +disabled = true +text = "Submit" + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="text_changed" from="MarginContainer/VBoxContainer/MessageSection/MessageEdit" to="." method="_on_message_edit_text_changed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Actions/CancelButton" to="." method="_on_cancel_button_pressed"] +[connection signal="pressed" from="MarginContainer/VBoxContainer/Actions/SubmitButton" to="." method="_on_submit_button_pressed"] diff --git a/project/addons/sentry/user_feedback/user_feedback_gui.gd b/project/addons/sentry/user_feedback/user_feedback_gui.gd new file mode 100644 index 00000000..4a12073c --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_gui.gd @@ -0,0 +1,183 @@ +@tool +extends Container +## Complete example of User Feedback integration. +## +## Usage: Add "user_feedback_gui.tscn" to your UI scene and call show() when needed. +## +## See "user_feedback_form.gd" for more details. + + +## Whether to display Sentry logo in the top right corner. +@export var logo_visible: bool = true: + set(value): + logo_visible = value + _update_form() + + +## Whether to display name input field. +@export var name_visible: bool = true: + set(value): + name_visible = value + _update_form() + +## Whether to display email input field. +@export var email_visible: bool = true: + set(value): + email_visible = value + _update_form() + +## Minimum number of words required in the feedback message before the feedback can be submitted. +@export var minimum_words: int = 2: + set(value): + minimum_words = value + _update_form() + +## Maximum size constraint for the feedback form (measured in reference resolution). +@export var maximum_form_size := Vector2(600, 600) + +## Vertical offset from the top edge of the container to position the form (measured in reference resolution). +@export var top_offset: float = 40.0 + + +@export_group("Auto Scale UI", "auto_scale") + +## Enabling this option allows feedback UI to scale for different resolutions. +## Note: The default theme is mastered for 1080p viewport resolution. +@export var auto_scale_enable: bool = true + +## Master resolution used as reference for UI scaling calculations. +## When auto_scale_enable is ON, the UI will scale proportionally based on +## the ratio between the current viewport height and this resolution. +@export var auto_scale_master_resolution: int = 1080 + + +@onready var _original_theme: Theme = theme + + +func _ready(): + _update_form() + + +func _gui_input(event: InputEvent) -> void: + if not visible: + return + if event is InputEventScreenTouch: + # Hide virtual keyboard when user taps outside the feedback UI. + DisplayServer.virtual_keyboard_hide() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_SORT_CHILDREN: + _resize_children() + + +func _update_form() -> void: + if is_node_ready(): + var form := %UserFeedbackForm + form.logo_visible = logo_visible + form.email_visible = email_visible + form.name_visible = name_visible + form.minimum_words = minimum_words + + +## Centers children horizontally at the top of the screen with an offset, +## and optionally rescales for actual viewport resolution. +func _resize_children() -> void: + var sz := get_size(); + + # Calculate scale factor + var scale_xy: float = 1.0 + if auto_scale_enable: + var vp_size: Vector2 = get_viewport().get_visible_rect().size + scale_xy = vp_size.y / auto_scale_master_resolution + _rescale_theme(scale_xy) + + for i in get_child_count(): + var c = get_child(i) + if c is not Control: + continue + + var new_sz := Vector2( + minf(sz.x, maximum_form_size.x * scale_xy), + minf(sz.y - top_offset * scale_xy, maximum_form_size.y * scale_xy)) + new_sz = new_sz.floor() + + var ofs: Vector2 = ((size - new_sz) / 2.0).floor() + ofs.y = floorf(top_offset * scale_xy) + + # Override size constraints when expand & fill flags are set to use all available space. + if c.size_flags_vertical == SIZE_EXPAND_FILL: + new_sz.y = size.y + ofs.y = 0 + if c.size_flags_horizontal == SIZE_EXPAND_FILL: + new_sz.x = size.x + ofs.x = 0 + + fit_child_in_rect(c, Rect2(ofs, new_sz)); + + +func _on_user_feedback_form_feedback_submitted(feedback: SentryFeedback) -> void: + hide() + + +func _on_user_feedback_form_feedback_cancelled() -> void: + hide() + + +# *** SCALING FOR DIFFERENT RESOLUTIONS *** + +func _rescale_theme(scale_factor: float) -> void: + if Engine.is_editor_hint() or not _original_theme: + # Don't make changes to theme in the editor. + return + + var th: Theme = _original_theme.duplicate() + + var font_size: int = th.default_font_size + th.default_font_size = floori(font_size * scale_factor) + th.set_font_size("font_size", "HeaderMedium", floori(font_size * 1.3 * scale_factor)) + + # Resize stylebox items + for theme_type in th.get_stylebox_type_list(): + for sb_name in th.get_stylebox_list(theme_type): + var sb: StyleBox = th.get_stylebox(sb_name, theme_type) + if sb is StyleBoxFlat: + th.set_stylebox(sb_name, theme_type, _scale_stylebox(sb, scale_factor)) + + # Resize constants + for theme_type in th.get_constant_type_list(): + for constant_name in th.get_constant_list(theme_type): + var c: int = th.get_constant(constant_name, theme_type) + c = floori(c * scale_factor) + th.set_constant(constant_name, theme_type, c) + + theme = th + + +func _scale_stylebox(sb: StyleBox, scale_factor: float) -> StyleBox: + if sb is StyleBoxFlat: + var new_sb: StyleBoxFlat = sb.duplicate() + + new_sb.content_margin_bottom = floorf(new_sb.content_margin_bottom * scale_factor) + new_sb.content_margin_right = floorf(new_sb.content_margin_right * scale_factor) + new_sb.content_margin_left = floorf(new_sb.content_margin_left * scale_factor) + new_sb.content_margin_top = floorf(new_sb.content_margin_top * scale_factor) + + new_sb.border_width_bottom = floorf(new_sb.border_width_bottom * scale_factor) + new_sb.border_width_top = floorf(new_sb.border_width_top * scale_factor) + new_sb.border_width_left = floorf(new_sb.border_width_left * scale_factor) + new_sb.border_width_right = floorf(new_sb.border_width_right * scale_factor) + + new_sb.corner_radius_bottom_left = floorf(new_sb.corner_radius_bottom_left * scale_factor) + new_sb.corner_radius_bottom_right = floorf(new_sb.corner_radius_bottom_right * scale_factor) + new_sb.corner_radius_top_left = floorf(new_sb.corner_radius_top_left * scale_factor) + new_sb.corner_radius_top_right = floorf(new_sb.corner_radius_top_right * scale_factor) + + new_sb.expand_margin_bottom = floorf(new_sb.expand_margin_bottom * scale_factor) + new_sb.expand_margin_left = floorf(new_sb.expand_margin_left * scale_factor) + new_sb.expand_margin_right = floorf(new_sb.expand_margin_right * scale_factor) + new_sb.expand_margin_top = floorf(new_sb.expand_margin_top * scale_factor) + + return new_sb + else: + return sb diff --git a/project/addons/sentry/user_feedback/user_feedback_gui.gd.uid b/project/addons/sentry/user_feedback/user_feedback_gui.gd.uid new file mode 100644 index 00000000..3bc2fde9 --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_gui.gd.uid @@ -0,0 +1 @@ +uid://b7c0cq2oneiwv diff --git a/project/addons/sentry/user_feedback/user_feedback_gui.tscn b/project/addons/sentry/user_feedback/user_feedback_gui.tscn new file mode 100644 index 00000000..baadd7a2 --- /dev/null +++ b/project/addons/sentry/user_feedback/user_feedback_gui.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=4 format=3 uid="uid://d3cll30toja8f"] + +[ext_resource type="Script" uid="uid://b7c0cq2oneiwv" path="res://addons/sentry/user_feedback/user_feedback_gui.gd" id="1_jugy2"] +[ext_resource type="PackedScene" uid="uid://bdn5fqm81rhy6" path="res://addons/sentry/user_feedback/user_feedback_form.tscn" id="1_t8jgq"] +[ext_resource type="Theme" uid="uid://bw0anqwp7xj8t" path="res://addons/sentry/user_feedback/sentry_theme.tres" id="1_u3uht"] + +[node name="UserFeedbackGUI" type="Container"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 0 +theme = ExtResource("1_u3uht") +script = ExtResource("1_jugy2") + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 1 +color = Color(0, 0, 0, 0.22745098) + +[node name="UserFeedbackForm" parent="." instance=ExtResource("1_t8jgq")] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 1 + +[node name="SubmitButton" parent="UserFeedbackForm/MarginContainer/VBoxContainer/Actions" index="1"] +theme_type_variation = &"ActionButton" + +[connection signal="feedback_cancelled" from="UserFeedbackForm" to="." method="_on_user_feedback_form_feedback_cancelled"] +[connection signal="feedback_submitted" from="UserFeedbackForm" to="." method="_on_user_feedback_form_feedback_submitted"] + +[editable path="UserFeedbackForm"] diff --git a/project/views/capture_events.gd b/project/views/capture_events.gd index cc7cefef..e6e2518b 100644 --- a/project/views/capture_events.gd +++ b/project/views/capture_events.gd @@ -9,13 +9,23 @@ extends VBoxContainer var _event_level: SentrySDK.Level +var _user_feedback_gui: Control + func _ready() -> void: - level_choice.get_popup().id_pressed.connect(_on_level_choice_id_pressed) + _init_user_feedback_gui() _init_level_choice_popup() _init_user_info() +## Initialize User Feedback UI +func _init_user_feedback_gui() -> void: + _user_feedback_gui = load("res://addons/sentry/user_feedback/user_feedback_gui.tscn").instantiate() + _user_feedback_gui.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + get_owner().add_child.call_deferred(_user_feedback_gui) + _user_feedback_gui.hide() + + func _init_level_choice_popup() -> void: var popup: PopupMenu = level_choice.get_popup() popup.add_item("DEBUG", SentrySDK.LEVEL_DEBUG) @@ -24,6 +34,7 @@ func _init_level_choice_popup() -> void: popup.add_item("ERROR", SentrySDK.LEVEL_ERROR) popup.add_item("FATAL", SentrySDK.LEVEL_FATAL) + popup.id_pressed.connect(_on_level_choice_id_pressed) _on_level_choice_id_pressed(SentrySDK.LEVEL_INFO) @@ -84,3 +95,7 @@ func _on_gen_script_error_pressed() -> void: func _on_gen_native_error_pressed() -> void: DemoOutput.print_info("Generating native Godot error (in C++ unit)...") load("res://file_does_not_exist") + + +func _on_user_feedback_button_pressed() -> void: + _user_feedback_gui.show() diff --git a/project/views/capture_events.tscn b/project/views/capture_events.tscn index d5283440..67aea3ab 100644 --- a/project/views/capture_events.tscn +++ b/project/views/capture_events.tscn @@ -83,6 +83,18 @@ text = "CAPTURE EVENTS" horizontal_alignment = 1 vertical_alignment = 2 +[node name="UserFeedback" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="Label" type="Label" parent="UserFeedback"] +layout_mode = 2 +text = "User feedback:" + +[node name="UserFeedbackButton" type="Button" parent="UserFeedback"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Show User Feedback Form" + [node name="MessageEvent" type="HBoxContainer" parent="."] layout_mode = 2 @@ -107,14 +119,14 @@ flat = false layout_mode = 2 text = "Capture Event" -[node name="Crash2" type="HBoxContainer" parent="."] +[node name="Crash" type="HBoxContainer" parent="."] layout_mode = 2 -[node name="Label" type="Label" parent="Crash2"] +[node name="Label" type="Label" parent="Crash"] layout_mode = 2 text = "Crash program: " -[node name="CrashButton" type="Button" parent="Crash2"] +[node name="CrashButton" type="Button" parent="Crash"] layout_mode = 2 size_flags_horizontal = 3 text = "CRASH!" @@ -122,5 +134,6 @@ text = "CRASH!" [connection signal="pressed" from="SetUserButton" to="." method="_on_set_user_button_pressed"] [connection signal="pressed" from="GenerateErrors/GenScriptError" to="." method="_on_gen_script_error_pressed"] [connection signal="pressed" from="GenerateErrors/GenNativeError" to="." method="_on_gen_native_error_pressed"] +[connection signal="pressed" from="UserFeedback/UserFeedbackButton" to="." method="_on_user_feedback_button_pressed"] [connection signal="pressed" from="MessageEvent/CaptureButton" to="." method="_on_capture_button_pressed"] -[connection signal="pressed" from="Crash2/CrashButton" to="." method="_on_crash_button_pressed"] +[connection signal="pressed" from="Crash/CrashButton" to="." method="_on_crash_button_pressed"]