399 lines
14 KiB
GDScript
399 lines
14 KiB
GDScript
class_name NetworkTab
|
|
extends VBoxContainer
|
|
|
|
const NetworkRequestItemScene = preload("res://Scenes/NetworkRequestItem.tscn")
|
|
|
|
@onready var filter_dropdown: OptionButton = %FilterDropdown
|
|
|
|
# Details panel components
|
|
@onready var close_button: Button = %CloseButton
|
|
@onready var details_tab_container: TabContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer
|
|
@onready var headers_tab: VBoxContainer = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Headers
|
|
@onready var preview_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Preview
|
|
@onready var response_tab: Control = $MainContainer/RightPanel/PanelContainer/HBoxContainer/TabContainer/Response
|
|
|
|
# Header components
|
|
@onready var status_header: Label = %StatusHeader
|
|
@onready var type_header: Label = %TypeHeader
|
|
@onready var size_header: Label = %SizeHeader
|
|
@onready var time_header: Label = %TimeHeader
|
|
|
|
# Main components
|
|
@onready var main_container: HSplitContainer = $MainContainer
|
|
@onready var request_list: VBoxContainer = $MainContainer/LeftPanel/ScrollContainer/RequestList
|
|
@onready var scroll_container: ScrollContainer = $MainContainer/LeftPanel/ScrollContainer
|
|
@onready var details_panel: Control = $MainContainer/RightPanel
|
|
@onready var status_bar: HBoxContainer = $HBoxContainer/StatusBar
|
|
@onready var request_count_label: Label = $HBoxContainer/StatusBar/RequestCount
|
|
@onready var transfer_label: Label = $HBoxContainer/StatusBar/Transfer
|
|
@onready var loaded_label: Label = $HBoxContainer/StatusBar/Loaded
|
|
|
|
@onready var syntax_highlighter = preload("res://Resources/LuaSyntaxHighlighter.tres")
|
|
|
|
var network_requests: Array[NetworkRequest] = []
|
|
var current_filter: NetworkRequest.RequestType = -1 # -1 means all
|
|
var selected_request: NetworkRequest = null
|
|
var request_items: Dictionary = {}
|
|
|
|
signal request_selected(request: NetworkRequest)
|
|
|
|
func _ready():
|
|
details_panel.visible = false
|
|
|
|
if main_container and main_container.size.x > 0:
|
|
main_container.split_offset = int(main_container.size.x)
|
|
|
|
update_status_bar()
|
|
|
|
NetworkManager.register_dev_tools_network_tab(self)
|
|
|
|
func add_network_request(request: NetworkRequest):
|
|
network_requests.append(request)
|
|
|
|
var request_item = NetworkRequestItemScene.instantiate() as NetworkRequestItem
|
|
request_list.add_child(request_item)
|
|
request_item.init(request, self)
|
|
request_item.item_clicked.connect(_on_request_item_clicked)
|
|
|
|
request_items[request.id] = request_item
|
|
|
|
apply_filter()
|
|
update_status_bar()
|
|
|
|
func apply_filter():
|
|
for request in network_requests:
|
|
var item = request_items.get(request.id)
|
|
if item:
|
|
var should_show = (current_filter == -1) or (request.type == current_filter)
|
|
item.visible = should_show
|
|
|
|
func update_request_item(request: NetworkRequest):
|
|
var request_item = request_items.get(request.id) as NetworkRequestItem
|
|
if not request_item:
|
|
return
|
|
|
|
request_item.update_display()
|
|
|
|
apply_filter()
|
|
update_status_bar()
|
|
|
|
func update_details_panel(request: NetworkRequest):
|
|
clear_details_panel()
|
|
update_headers_tab(request)
|
|
update_preview_tab(request)
|
|
update_response_tab(request)
|
|
|
|
func clear_details_panel():
|
|
for child in headers_tab.get_children(): child.queue_free()
|
|
for child in preview_tab.get_children(): child.queue_free()
|
|
for child in response_tab.get_children(): child.queue_free()
|
|
|
|
func create_collapsible_section(title: String, expanded: bool = false) -> VBoxContainer:
|
|
var section = VBoxContainer.new()
|
|
|
|
# Header w/ toggle button
|
|
var header = HBoxContainer.new()
|
|
header.custom_minimum_size.y = 28
|
|
|
|
var toggle_button = Button.new()
|
|
toggle_button.text = "▼" if expanded else "▶"
|
|
toggle_button.custom_minimum_size = Vector2(20, 20)
|
|
toggle_button.flat = true
|
|
toggle_button.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
|
header.add_child(toggle_button)
|
|
|
|
var title_label = Label.new()
|
|
title_label.text = title
|
|
title_label.add_theme_font_size_override("font_size", 14)
|
|
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
header.add_child(title_label)
|
|
|
|
section.add_child(header)
|
|
|
|
# Content container
|
|
var content = VBoxContainer.new()
|
|
content.visible = expanded
|
|
section.add_child(content)
|
|
|
|
toggle_button.pressed.connect(func():
|
|
content.visible = !content.visible
|
|
toggle_button.text = "▼" if content.visible else "▶"
|
|
)
|
|
|
|
return section
|
|
|
|
func add_header_row(parent: VBoxContainer, header_name: String, value: String):
|
|
var row = HBoxContainer.new()
|
|
|
|
var name_label = Label.new()
|
|
name_label.text = header_name
|
|
name_label.custom_minimum_size.x = 200
|
|
name_label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
name_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
|
name_label.clip_text = true
|
|
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
|
row.add_child(name_label)
|
|
|
|
var value_label = Label.new()
|
|
value_label.text = value
|
|
value_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
value_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
value_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
|
row.add_child(value_label)
|
|
|
|
parent.add_child(row)
|
|
|
|
func update_headers_tab(request: NetworkRequest):
|
|
var general_section = create_collapsible_section("General", true)
|
|
headers_tab.add_child(general_section)
|
|
|
|
var general_content = general_section.get_child(1)
|
|
add_header_row(general_content, "Request URL:", request.url)
|
|
add_header_row(general_content, "Request Method:", request.method)
|
|
add_header_row(general_content, "Status Code:", str(request.status_code) + " " + request.status_text)
|
|
|
|
# Request Headers section
|
|
if not request.request_headers.is_empty():
|
|
var request_headers_section = create_collapsible_section("Request Headers", false)
|
|
headers_tab.add_child(request_headers_section)
|
|
|
|
var request_headers_content = request_headers_section.get_child(1)
|
|
for header_name in request.request_headers:
|
|
add_header_row(request_headers_content, header_name + ":", str(request.request_headers[header_name]))
|
|
|
|
# Response Headers section
|
|
if not request.response_headers.is_empty():
|
|
var response_headers_section = create_collapsible_section("Response Headers", false)
|
|
headers_tab.add_child(response_headers_section)
|
|
|
|
var response_headers_content = response_headers_section.get_child(1)
|
|
for header_name in request.response_headers:
|
|
add_header_row(response_headers_content, header_name + ":", str(request.response_headers[header_name]))
|
|
|
|
func update_preview_tab(request: NetworkRequest):
|
|
# For images, show the image in the preview tab
|
|
if request.type == NetworkRequest.RequestType.IMG and request.status == NetworkRequest.RequestStatus.SUCCESS:
|
|
var image = Image.new()
|
|
var response_bytes = request.response_body_bytes
|
|
var load_error = ERR_UNAVAILABLE
|
|
|
|
load_error = image.load_png_from_buffer(response_bytes)
|
|
if load_error != OK:
|
|
load_error = image.load_jpg_from_buffer(response_bytes)
|
|
if load_error != OK:
|
|
load_error = image.load_webp_from_buffer(response_bytes)
|
|
if load_error != OK:
|
|
load_error = image.load_bmp_from_buffer(response_bytes)
|
|
if load_error != OK:
|
|
load_error = image.load_tga_from_buffer(response_bytes)
|
|
|
|
if load_error == OK:
|
|
var texture = ImageTexture.create_from_image(image)
|
|
|
|
var container = VBoxContainer.new()
|
|
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
container.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
|
|
var texture_rect = TextureRect.new()
|
|
texture_rect.texture = texture
|
|
texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
|
texture_rect.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
texture_rect.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
|
|
|
var img_size = image.get_size()
|
|
var max_size = 200.0
|
|
var scale_factor = min(max_size / img_size.x, max_size / img_size.y, 1.0)
|
|
texture_rect.custom_minimum_size = Vector2(img_size.x * scale_factor, img_size.y * scale_factor)
|
|
|
|
container.add_child(texture_rect)
|
|
preview_tab.add_child(container)
|
|
return
|
|
else:
|
|
var label = Label.new()
|
|
label.text = "Failed to load image data (Error: " + str(load_error) + ")"
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
preview_tab.add_child(label)
|
|
return
|
|
|
|
# For non-images, show request body
|
|
if request.request_body.is_empty():
|
|
var label = Label.new()
|
|
label.text = "No request body"
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
preview_tab.add_child(label)
|
|
return
|
|
|
|
# CodeEdit for request body
|
|
# TODO: Syntax highlight based on Content-Type, we need a JSON, HTML and CSS highlighter too
|
|
var code_edit = CodeEditUtils.create_code_edit({
|
|
"text": request.request_body,
|
|
"editable": false,
|
|
"show_line_numbers": true,
|
|
"syntax_highlighter": syntax_highlighter.duplicate()
|
|
})
|
|
|
|
preview_tab.add_child(code_edit)
|
|
|
|
func update_response_tab(request: NetworkRequest):
|
|
if request.type == NetworkRequest.RequestType.IMG:
|
|
var label = Label.new()
|
|
label.text = "This response contains image data. See the \"Preview\" tab to view the image."
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
response_tab.add_child(label)
|
|
return
|
|
|
|
if request.response_body.is_empty():
|
|
var label = Label.new()
|
|
label.text = "No response body"
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
response_tab.add_child(label)
|
|
return
|
|
|
|
# Check if we can display the content
|
|
var can_display = true
|
|
if request.mime_type.begins_with("video/") or request.mime_type.begins_with("audio/"):
|
|
can_display = false
|
|
|
|
if not can_display:
|
|
var label = Label.new()
|
|
label.text = "Cannot preview this content type: " + request.mime_type
|
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
response_tab.add_child(label)
|
|
return
|
|
|
|
# Create CodeEdit for response body
|
|
var code_edit = CodeEditUtils.create_code_edit({
|
|
"text": request.response_body,
|
|
"editable": false,
|
|
"show_line_numbers": true,
|
|
"syntax_highlighter": syntax_highlighter.duplicate()
|
|
})
|
|
|
|
response_tab.add_child(code_edit)
|
|
|
|
func update_status_bar():
|
|
var total_requests = network_requests.size()
|
|
var total_size = 0
|
|
var loaded_resources = 0
|
|
|
|
for request in network_requests:
|
|
total_size += request.size
|
|
if request.status == NetworkRequest.RequestStatus.SUCCESS:
|
|
loaded_resources += 1
|
|
|
|
request_count_label.text = str(total_requests) + " requests"
|
|
transfer_label.text = NetworkRequest.format_bytes(total_size) + " transferred"
|
|
loaded_label.text = str(loaded_resources) + " resources loaded"
|
|
|
|
func hide_details_panel():
|
|
# Hide details panel and show columns again
|
|
details_panel.visible = false
|
|
hide_columns(false)
|
|
selected_request = null
|
|
if main_container.size.x > 0:
|
|
main_container.split_offset = int(main_container.size.x)
|
|
|
|
# Clear selection visual
|
|
for req_id in request_items:
|
|
var request_item = request_items[req_id] as NetworkRequestItem
|
|
request_item.set_selected(false)
|
|
|
|
func hide_columns(should_hide: bool):
|
|
# Hide/show header labels
|
|
status_header.visible = !should_hide
|
|
type_header.visible = !should_hide
|
|
size_header.visible = !should_hide
|
|
time_header.visible = !should_hide
|
|
|
|
# Hide/show status, type, size, time columns for all request items
|
|
for request_item in request_items.values():
|
|
var network_request_item = request_item as NetworkRequestItem
|
|
network_request_item.hide_columns(should_hide)
|
|
|
|
func clear_all_requests():
|
|
for item in request_items.values():
|
|
item.queue_free()
|
|
|
|
network_requests.clear()
|
|
request_items.clear()
|
|
selected_request = null
|
|
|
|
# Hide details panel and show columns again
|
|
details_panel.visible = false
|
|
hide_columns(false)
|
|
if main_container.size.x > 0:
|
|
main_container.split_offset = int(main_container.size.x)
|
|
|
|
update_status_bar()
|
|
|
|
func clear_all_requests_except(preserve_request_id: String):
|
|
# Remove all items except the preserved one
|
|
var preserved_request = null
|
|
var preserved_item = null
|
|
|
|
for request in network_requests:
|
|
if request.id == preserve_request_id:
|
|
preserved_request = request
|
|
preserved_item = request_items.get(preserve_request_id)
|
|
break
|
|
|
|
# Clear all items except preserved one
|
|
for item_id in request_items:
|
|
if item_id != preserve_request_id:
|
|
var item = request_items[item_id]
|
|
item.queue_free()
|
|
|
|
network_requests.clear()
|
|
request_items.clear()
|
|
|
|
# Re-add preserved request and item
|
|
if preserved_request and preserved_item:
|
|
network_requests.append(preserved_request)
|
|
request_items[preserve_request_id] = preserved_item
|
|
|
|
selected_request = null
|
|
|
|
# Hide details panel and show columns again
|
|
details_panel.visible = false
|
|
hide_columns(false)
|
|
if main_container.size.x > 0:
|
|
main_container.split_offset = int(main_container.size.x)
|
|
|
|
update_status_bar()
|
|
|
|
func _on_request_item_clicked(request: NetworkRequest):
|
|
if selected_request == request:
|
|
hide_details_panel()
|
|
return
|
|
|
|
selected_request = request
|
|
request_selected.emit(request)
|
|
|
|
for req_id in request_items:
|
|
var request_item = request_items[req_id] as NetworkRequestItem
|
|
request_item.set_selected(req_id == request.id)
|
|
|
|
details_panel.visible = true
|
|
if main_container.size.x > 0:
|
|
# Give 6/8 (3/4) of space to details panel, 2/8 (1/4) to left panel
|
|
main_container.split_offset = int(main_container.size.x * 0.25)
|
|
|
|
hide_columns(true)
|
|
update_details_panel(request)
|
|
|
|
func _on_filter_selected(index: int):
|
|
var filter_type = index - 1 # 0 -> -1 (All), 1 -> 0 (Fetch)...
|
|
|
|
if current_filter == filter_type:
|
|
return
|
|
|
|
current_filter = filter_type
|
|
apply_filter()
|