initial commit

This commit is contained in:
2026-07-04 18:22:23 -06:00
commit 55522fc6fc
272 changed files with 12391 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8
+2
View File
@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
+3
View File
@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/
+31
View File
@@ -0,0 +1,31 @@
extends Node
var M2FT: float = 3.28084
var FT2M: float = 1/3.28084
var WGS84_A_M = 6378137.0
var WGS84_B_M = 6356752.314245
var EARTH_RADIUS_M = (2*WGS84_A_M + WGS84_B_M) / 3
var EARTH_RADIUS_KM = EARTH_RADIUS_M / 1000
# 8171221.7221355
var WGS84_AB = WGS84_A_M * WGS84_B_M
var g = 9.80665
func calc_time_diff(timestamp_1, timestamp_2):
if timestamp_2 < 3600 and timestamp_1 > 82800:
return (timestamp_2 + 86400) - timestamp_1
return timestamp_2 - timestamp_1
func is_timestamp_newer(old_timestamp: float, new_timestamp: float) -> bool:
if old_timestamp > 82800. and new_timestamp < 3600: # earlier than 23 hours
return true
return old_timestamp < new_timestamp
func calc_wgs84_altitude(latitude_rad: float):
var theta_rad = abs(latitude_rad)
var a_sin_2 = (WGS84_A_M ** 2) * (sin(theta_rad)**2)
var b_cos_2 = (WGS84_B_M ** 2) * (cos(theta_rad)**2)
return WGS84_AB / sqrt(a_sin_2 + b_cos_2)
+1
View File
@@ -0,0 +1 @@
uid://qhcebwlrvy1s
+14
View File
@@ -0,0 +1,14 @@
extends Node
var WINDOW_SIZE = Vector2.ZERO
var WINDOW_CENTER = Vector2.ZERO
var DISPLAY_MODES = ["CIRCLE", "RECTANGLE"]
var DISPLAY_MODE_IDX = 0
var DISPLAY_PARAMETERS = [0, 0]
var TL_CORNER = Vector2(-1., -1.).normalized()
var TR_CORNER = Vector2(1., -1.).normalized()
var BL_CORNER = Vector2(-1., 1.).normalized()
var BR_CORNER = Vector2(1., 1.).normalized()
+1
View File
@@ -0,0 +1 @@
uid://xunxbytiofa2
+68
View File
@@ -0,0 +1,68 @@
extends Node
func line(pos1: Vector3, pos2: Vector3, color = Color.WHITE_SMOKE, persist_ms = 0):
var mesh_instance := MeshInstance3D.new()
var immediate_mesh := ImmediateMesh.new()
var material := ORMMaterial3D.new()
mesh_instance.mesh = immediate_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
immediate_mesh.surface_begin(Mesh.PRIMITIVE_LINES, material)
immediate_mesh.surface_add_vertex(pos1)
immediate_mesh.surface_add_vertex(pos2)
immediate_mesh.surface_end()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
return await final_cleanup(mesh_instance, persist_ms)
func point(pos: Vector3, radius = 0.05, color = Color.WHITE_SMOKE, persist_ms = 0):
var mesh_instance := MeshInstance3D.new()
var sphere_mesh := SphereMesh.new()
var material := ORMMaterial3D.new()
mesh_instance.mesh = sphere_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
mesh_instance.position = pos
sphere_mesh.radius = radius
sphere_mesh.height = radius*2
sphere_mesh.material = material
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
return await final_cleanup(mesh_instance, persist_ms)
func square(pos: Vector3, size: Vector2, color = Color.WHITE_SMOKE, persist_ms = 0):
var mesh_instance := MeshInstance3D.new()
var box_mesh := BoxMesh.new()
var material := ORMMaterial3D.new()
mesh_instance.mesh = box_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
mesh_instance.position = pos
box_mesh.size = Vector3(size.x, size.y, 1)
box_mesh.material = material
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
return await final_cleanup(mesh_instance, persist_ms)
## 1 -> Lasts ONLY for current physics frame
## >1 -> Lasts X time duration.
## <1 -> Stays indefinitely
func final_cleanup(mesh_instance: MeshInstance3D, persist_ms: float):
get_tree().get_root().add_child(mesh_instance)
if persist_ms == 1:
await get_tree().physics_frame
mesh_instance.queue_free()
elif persist_ms > 0:
await get_tree().create_timer(persist_ms).timeout
mesh_instance.queue_free()
else:
return mesh_instance
+1
View File
@@ -0,0 +1 @@
uid://ct37h7r6osrbn
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 minoqi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+55
View File
@@ -0,0 +1,55 @@
[gd_scene format=3 uid="uid://cckxwooodbwpx"]
[ext_resource type="Script" uid="uid://csg4l1dtlx5u1" path="res://networking/scripts/udp_broadcast.gd" id="1_glv2v"]
[ext_resource type="Script" uid="uid://ddil0ki5etrwp" path="res://sensor_approval/SensorApprovalController.gd" id="1_trn2v"]
[ext_resource type="Script" uid="uid://b31uplppp5cb0" path="res://networking/scripts/udp_server.gd" id="2_uu6xs"]
[ext_resource type="PackedScene" uid="uid://c2yga5koqwo72" path="res://views/2d/View2D.tscn" id="7_lgr22"]
[ext_resource type="PackedScene" uid="uid://bdpu08j1vptl8" path="res://sensor_approval/SensorApproval.tscn" id="8_03owx"]
[ext_resource type="Script" uid="uid://tx2flx7kdttv" path="res://views/ViewController.gd" id="8_wkp8b"]
[ext_resource type="Script" uid="uid://bcubn1h3e5j6a" path="res://settings.gd" id="10_5vvyt"]
[ext_resource type="Script" uid="uid://d0rlx8r750u05" path="res://vehicle/scripts/vehicle_spawner.gd" id="10_20pc6"]
[ext_resource type="PackedScene" uid="uid://crpqmmpxfe2yi" path="res://views/3d/View3D.tscn" id="10_wkp8b"]
[ext_resource type="PackedScene" uid="uid://cf2vkjpwwdog7" path="res://vehicle/vehicle.tscn" id="11_5vvyt"]
[ext_resource type="PackedScene" uid="uid://blijvkgrhoas" path="res://settings/SettingsMenu.tscn" id="11_20pc6"]
[node name="Main" type="Node" unique_id=247156581]
[node name="Networking" type="Node" parent="." unique_id=2036280046]
[node name="UDPBroadcast" type="Node" parent="Networking" unique_id=391407292]
script = ExtResource("1_glv2v")
[node name="UDPServer" type="Node" parent="Networking" unique_id=1621629338]
script = ExtResource("2_uu6xs")
[node name="VehicleSpawner" type="Node" parent="." unique_id=2034195923]
script = ExtResource("10_20pc6")
vehicle_scene = ExtResource("11_5vvyt")
[node name="SensorApprovalController" type="VBoxContainer" parent="." unique_id=238699571]
offset_left = 118.0
offset_top = 232.0
offset_right = 158.0
offset_bottom = 272.0
script = ExtResource("1_trn2v")
sensor_approval_scene = ExtResource("8_03owx")
metadata/_custom_type_script = "uid://ddil0ki5etrwp"
[node name="ViewController" type="Node" parent="." unique_id=399154406]
script = ExtResource("8_wkp8b")
view_2d = ExtResource("7_lgr22")
view_3d = ExtResource("10_wkp8b")
show_view_2d = true
[node name="Settings" type="Node" parent="." unique_id=1797069457]
script = ExtResource("10_5vvyt")
[node name="OpenSettings" type="Button" parent="Settings" unique_id=2042815305]
offset_right = 8.0
offset_bottom = 8.0
text = "Settings"
[node name="SettingsMenu" parent="Settings" unique_id=1877869509 instance=ExtResource("11_20pc6")]
[connection signal="pressed" from="Settings/OpenSettings" to="ViewController" method="_on_open_settings_pressed"]
[connection signal="pressed" from="Settings/OpenSettings" to="Settings" method="_on_settings_opened"]
+13
View File
@@ -0,0 +1,13 @@
[gd_scene format=3 uid="uid://cabkwpatclndg"]
[ext_resource type="Script" uid="uid://ba2mpftq0816s" path="res://map_viewer.gd" id="1_u636o"]
[node name="MapView" type="Node2D" unique_id=771852762]
[node name="Control" type="Control" parent="." unique_id=105999415]
layout_mode = 3
anchors_preset = 0
offset_right = 40.0
offset_bottom = 40.0
mouse_filter = 1
script = ExtResource("1_u636o")
+28
View File
@@ -0,0 +1,28 @@
extends Control
class_name MovableControl
@export_category("Logic")
@export var move_target: BaseWidgetNode
var is_hovering: bool
var is_grab: bool
var mouse_down_pos: Vector2
func _ready() -> void:
mouse_entered.connect(func(): is_hovering = true)
mouse_exited.connect(func(): is_hovering = false)
func _process(delta: float) -> void:
#print(is_hovering)
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and is_hovering and not is_grab:
mouse_down_pos = get_viewport().get_mouse_position()
is_grab = true
if is_grab and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
is_grab = false
if is_grab:
var current_mouse_pos: Vector2 = get_global_mouse_position()
var offset: Vector2 = current_mouse_pos - mouse_down_pos
move_target.position += offset
mouse_down_pos = current_mouse_pos
+1
View File
@@ -0,0 +1 @@
uid://cr4y57bw6jfd1
+32
View File
@@ -0,0 +1,32 @@
<img src="Promotion/minosUUIDGeneratorForGodotIcon.svg" width="128" height="128">
# [Minos UUID Generator](https://minosuuidgenerator.carrd.co) for Godot
A UUID generator for [Godot Engine](https://godotengine.org/), with 1.295×10^50 combinations it'll work for any sized game, never having to worry about running out of unique IDs! Automatically stores new UUIDs and removes old ones.
*NOTE: This is for Godot 4 only, and only supports GDScript as of right now (C# on the way!)*
## Features
- Custom node (UUIDManager) that stores the UUID
- Automatically generates UUID when node is created
- Automatically removes UUID when node is deleted
- (Optional) debug messages to make sure UUIDs are always working
- Easily create UUIDs at runtime, just need to instantiate the node and the rest is done automatically
## Installation
Copy the `minos_uuid_generator` directory into your `res://addons/` directory. Then make sure to enable the plugin in `Project Settings -> Plugins`.
## Documentation
- [GDScript](Documentation/GDScriptDocumentation.md)
- C# Coming soon!
## Contributors
Minos UUID Generator for Godot is made by [Minoqi](https://freelancefelix.com).
## License
Licensed under the MIT license, see `LICENSE` for more information.
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ivppb5cqrsak"
path="res://.godot/imported/NinePatchWindow-InnerRecess.png-07bd09e8d845ac941af5cf12e9f19c54.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/NinePatchWindow-InnerRecess.png"
dest_files=["res://.godot/imported/NinePatchWindow-InnerRecess.png-07bd09e8d845ac941af5cf12e9f19c54.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1w4w4yi6p3w3"
path="res://.godot/imported/NinePatchWindowInner.png-47f4d96fc18c0a2a90855797a8cd0e05.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/NinePatchWindowInner.png"
dest_files=["res://.godot/imported/NinePatchWindowInner.png-47f4d96fc18c0a2a90855797a8cd0e05.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cs676riq457do"
path="res://.godot/imported/NinePatchWindowOuter.png-bca6314f960490a8a5e8b837d843d94e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/NinePatchWindowOuter.png"
dest_files=["res://.godot/imported/NinePatchWindowOuter.png-bca6314f960490a8a5e8b837d843d94e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bhps6csrl6lef"
path="res://.godot/imported/close.png-9f5b597652eecfc041a6acfb8a79a5f3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/close.png"
dest_files=["res://.godot/imported/close.png-9f5b597652eecfc041a6acfb8a79a5f3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckoblpvpivjma"
path="res://.godot/imported/icon.png-b025e725a72f01c7e881efaf61058c1e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/icon.png"
dest_files=["res://.godot/imported/icon.png-b025e725a72f01c7e881efaf61058c1e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
@@ -0,0 +1,17 @@
[gd_resource type="StyleBoxTexture" load_steps=2 format=3 uid="uid://b7k8yo7jfhpue"]
[ext_resource type="Texture2D" uid="uid://d1w4w4yi6p3w3" path="res://addons/loggie-console/assets/NinePatchWindowInner.png" id="1_vngi6"]
[resource]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
texture = ExtResource("1_vngi6")
texture_margin_left = 2.0
texture_margin_top = 2.0
texture_margin_right = 2.0
texture_margin_bottom = 2.0
axis_stretch_horizontal = 1
axis_stretch_vertical = 1
region_rect = Rect2(0, 0, 36, 36)
@@ -0,0 +1,16 @@
[gd_resource type="StyleBoxTexture" load_steps=2 format=3 uid="uid://dolgdjpxlvt8f"]
[ext_resource type="Texture2D" uid="uid://ivppb5cqrsak" path="res://addons/loggie-console/assets/NinePatchWindow-InnerRecess.png" id="1_lfme6"]
[resource]
content_margin_left = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
texture = ExtResource("1_lfme6")
texture_margin_left = 2.0
texture_margin_top = 2.0
texture_margin_right = 2.0
texture_margin_bottom = 2.0
axis_stretch_horizontal = 1
axis_stretch_vertical = 1
region_rect = Rect2(0, 0, 36, 36)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2ie83nlho48h"
path="res://.godot/imported/minimize.png-3f9dff399b51d5e6e7a64266986b96cf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie-console/assets/minimize.png"
dest_files=["res://.godot/imported/minimize.png-3f9dff399b51d5e6e7a64266986b96cf.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
@@ -0,0 +1,13 @@
[gd_resource type="StyleBoxTexture" load_steps=2 format=3 uid="uid://bpkqcbx15ejak"]
[ext_resource type="Texture2D" uid="uid://cs676riq457do" path="res://sprites/ui/NinePatchWindowBrown.png" id="1_4h7p8"]
[resource]
texture = ExtResource("1_4h7p8")
texture_margin_left = 2.0
texture_margin_top = 2.0
texture_margin_right = 2.0
texture_margin_bottom = 2.0
axis_stretch_horizontal = 1
axis_stretch_vertical = 1
region_rect = Rect2(0, 0, 36, 36)
@@ -0,0 +1,21 @@
[gd_resource type="StyleBoxTexture" load_steps=2 format=3 uid="uid://cfafk3ybc15uo"]
[ext_resource type="Texture2D" uid="uid://cs676riq457do" path="res://sprites/ui/NinePatchWindowBrown.png" id="1_8qfda"]
[resource]
content_margin_left = 2.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
texture = ExtResource("1_8qfda")
texture_margin_left = 2.0
texture_margin_top = 2.0
texture_margin_right = 2.0
texture_margin_bottom = 2.0
expand_margin_left = 4.0
expand_margin_top = 24.0
expand_margin_right = 4.0
expand_margin_bottom = 4.0
axis_stretch_horizontal = 1
axis_stretch_vertical = 1
region_rect = Rect2(0, 0, 36, 36)
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="Loggie Console"
description="Interactive debug console built upon the Loggie logging framework"
author="Smitner Studio"
version="1.0"
script="plugin.gd"
+16
View File
@@ -0,0 +1,16 @@
@tool
class_name LoggieConsoleEditorPlugin extends EditorPlugin
const AUTOLOAD_NAME = &"LoggieConsoleAutoload"
func _enter_tree():
add_autoload_singleton(AUTOLOAD_NAME, "res://addons/loggie-console/scenes/console.tscn")
func _exit_tree():
remove_autoload_singleton(AUTOLOAD_NAME)
func _enable_plugin() -> void:
print("%s enabled" % AUTOLOAD_NAME)
func _disable_plugin() -> void:
print("%s disabled" % AUTOLOAD_NAME)
+1
View File
@@ -0,0 +1 @@
uid://b8ul8pbaeqx2l
@@ -0,0 +1,55 @@
@tool
class_name LoggieConsoleFilterState extends Resource
## Persistent filter state resource for LoggieConsole
##
## Stores filtering criteria (domains, log levels, search terms) in a format compatible
## with Godot's Resource system while maintaining compatibility with LogBuffer.FilterState.
## This enables console filter settings to persist between application sessions.
## List of domain names that are currently enabled for display
@export var enabled_domains: Array[String] = []
## Minimum log level to display (stored as int for persistence compatibility)
@export var min_log_level: int = LoggieEnums.LogLevel.DEBUG
## Text search filter - only messages containing this text will be shown
@export var text_search: String = ""
## Whether to show only messages that include stack traces
@export var show_stack_only: bool = false
## Converts this persistent resource to runtime FilterState object
## Returns: A new LogBuffer.FilterState instance with matching filter criteria
func to_filter_state() -> LogBuffer.FilterState:
var filter_state: LogBuffer.FilterState = LogBuffer.FilterState.new()
filter_state.enabled_domains = enabled_domains.duplicate()
filter_state.min_log_level = min_log_level as LoggieEnums.LogLevel
filter_state.text_search = text_search
filter_state.show_stack_only = show_stack_only
return filter_state
## Populates this resource from runtime FilterState object
## Param filter_state: The LogBuffer.FilterState to copy data from
func from_filter_state(filter_state: LogBuffer.FilterState) -> void:
enabled_domains = filter_state.enabled_domains.duplicate()
min_log_level = filter_state.min_log_level as int
text_search = filter_state.text_search
show_stack_only = filter_state.show_stack_only
## Validates and sanitizes all data after loading from disk
## Ensures all fields have valid values even if the saved file was corrupted
func validate() -> void:
# Ensure domain array exists
if enabled_domains == null:
enabled_domains = []
# Validate log level is within valid enum range
if min_log_level < 0 or min_log_level >= LoggieEnums.LogLevel.size():
min_log_level = LoggieEnums.LogLevel.DEBUG
Loggie.msg("Invalid log level, reset to DEBUG").domain(LoggieConsoleConstants.DOMAIN).warn()
# Ensure search text is never null
if text_search == null:
text_search = ""
@@ -0,0 +1 @@
uid://b6iuus8kvu3y3
@@ -0,0 +1,294 @@
@tool
class_name LoggieConsoleSettings extends Resource
## Complete persistent settings for LoggieConsole
##
## This resource stores all console state including window properties, filter settings,
## component states, and discovered domains. It automatically saves when the console
## exits and loads on startup to restore the user's previous console configuration.
const LoggieConsoleFilterState = preload("res://addons/loggie-console/resources/loggie_console_filter_state.gd")
# Configuration constants
const MIN_WINDOW_WIDTH: int = 400
const MIN_WINDOW_HEIGHT: int = 300
const DEFAULT_WINDOW_WIDTH: int = 800
const DEFAULT_WINDOW_HEIGHT: int = 400
const SCREEN_MARGIN_PIXELS: int = 100 # 50px on each side
const WINDOW_EDGE_MARGIN: int = 20
const WINDOW_MIN_MARGIN: int = 10
## Filter state for log message display (domains, levels, search terms)
@export var filter_state: LoggieConsoleFilterState
## Currently enabled domain names for filtering
@export var enabled_domains: Array[String] = []
## All domain names discovered during runtime (persisted for next session)
@export var all_known_domains: Array[String] = []
## Console window screen position (-1, -1 means use default centering)
@export var window_position: Vector2i = Vector2i(-1, -1)
## Console window size in pixels
@export var window_size: Vector2i = Vector2i(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
## Whether console is currently minimized
@export var is_minimized: bool = false
## Whether console window is visible
@export var is_visible: bool = true
## Display/screen index where console was last positioned (-1 means primary display)
@export var display_index: int = -1
## Whether subwindows were embedded when settings were last saved
@export var was_embedded_subwindows: bool = false
## Current log level filter (stored as int for persistence)
@export var log_level: int = LoggieEnums.LogLevel.DEBUG
## Whether to auto-scroll to newest messages
@export var scroll_follow_enabled: bool = true
## Current text search filter term
@export var search_text: String = ""
## Whether to show only messages with stack traces
@export var stack_filter_enabled: bool = false
## Maximum number of messages to keep in buffer before cleanup
@export var max_messages: int = 5000
## Text size multiplier for console fonts (0.5 to 2.0, default 1.0)
@export var text_size_multiplier: float = 1.0
## Restore button alignment position (0=TOP_LEFT, 1=TOP_RIGHT, 2=BOTTOM_LEFT, 3=BOTTOM_RIGHT)
@export var restore_button_alignment: int = 1 # Default to TOP_RIGHT
## Settings file format version for future migration compatibility
@export var version: int = 1
## Unix timestamp when settings were last saved
@export var last_saved: float = 0.0
## Initializes default settings values
##
## Creates the filter state resource if it doesn't exist. This ensures that
## all required sub-resources are properly initialized even when creating
## settings from scratch.
func _init() -> void:
if not filter_state:
filter_state = LoggieConsoleFilterState.new()
## Validates and sanitizes all data after loading from disk
## Ensures all fields have valid values even if the saved file was corrupted
func validate() -> void:
# Ensure filter state exists and is valid
if not filter_state:
filter_state = LoggieConsoleFilterState.new()
filter_state.validate()
# Ensure domain arrays exist
if enabled_domains == null:
enabled_domains = []
if all_known_domains == null:
all_known_domains = []
# Validate log level is within enum range
if log_level < 0 or log_level >= LoggieEnums.LogLevel.size():
log_level = LoggieEnums.LogLevel.DEBUG
Loggie.msg("Invalid log level, reset to DEBUG").domain(LoggieConsoleConstants.DOMAIN).warn()
# Enforce minimum and maximum window size for usability
_validate_window_size()
# Ensure search text is never null
if search_text == null:
search_text = ""
# Validate text size multiplier is within reasonable bounds
if text_size_multiplier < 0.5 or text_size_multiplier > 2.0:
text_size_multiplier = 1.0
Loggie.msg("Invalid text size multiplier, reset to 1.0").domain(LoggieConsoleConstants.DOMAIN).warn()
# Validate restore button alignment is within enum range (0-3)
if restore_button_alignment < 0 or restore_button_alignment > 3:
restore_button_alignment = 1 # Default to TOP_RIGHT
Loggie.msg("Invalid restore button alignment, reset to TOP_RIGHT").domain(LoggieConsoleConstants.DOMAIN).warn()
# Update save timestamp
last_saved = Time.get_unix_time_from_system()
## Validates and ensures window size fits within available screen space
##
## Enforces minimum size for usability (400x300) and maximum size based on
## screen dimensions. Checks all connected displays to find the largest
## available space and clamps the window size accordingly.
func _validate_window_size() -> void:
# Get the largest available screen to determine maximum constraints
var max_screen_size: Vector2i = Vector2i.ZERO
var screens: int = DisplayServer.get_screen_count()
for i: int in screens:
var screen_rect: Rect2i = DisplayServer.screen_get_usable_rect(i)
max_screen_size.x = maxi(max_screen_size.x, screen_rect.size.x)
max_screen_size.y = maxi(max_screen_size.y, screen_rect.size.y)
# Fallback if no screens detected
if max_screen_size == Vector2i.ZERO:
max_screen_size = Vector2i(1920, 1080)
# Enforce minimum window size for usability
window_size.x = maxi(window_size.x, MIN_WINDOW_WIDTH)
window_size.y = maxi(window_size.y, MIN_WINDOW_HEIGHT)
# Enforce maximum window size to fit on screen (with margins)
var max_width: int = max_screen_size.x - SCREEN_MARGIN_PIXELS
var max_height: int = max_screen_size.y - SCREEN_MARGIN_PIXELS
if window_size.x > max_width:
window_size.x = max_width
Loggie.msg("Window width too large, clamped to %d" % max_width).domain(LoggieConsoleConstants.DOMAIN).warn()
if window_size.y > max_height:
window_size.y = max_height
Loggie.msg("Window height too large, clamped to %d" % max_height).domain(LoggieConsoleConstants.DOMAIN).warn()
## Detects if embed_subwindows setting has changed since last save
## Returns: true if setting has changed and window positioning needs reset
func _has_embed_setting_changed() -> bool:
var current_embedded = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows", true)
return current_embedded != was_embedded_subwindows
## Updates the stored embed_subwindows setting to current project setting
##
## This should be called whenever settings are saved to track the current
## embed_subwindows state for future change detection.
func update_embed_setting() -> void:
was_embedded_subwindows = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows", true)
## Validates and corrects window position for current display setup
## Returns: A valid window position that will be visible on screen
func validate_window_position() -> Vector2i:
# Check if embed_subwindows setting has changed
var embed_changed = _has_embed_setting_changed()
var current_embedded = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows", true)
# If embed setting changed and window was previously invisible, reset position
if embed_changed and not is_visible:
Loggie.msg("embed_subwindows changed from %s to %s, resetting window position" % [was_embedded_subwindows, current_embedded]).domain(LoggieConsoleConstants.DOMAIN).warn()
update_embed_setting()
# Reset to default centering
var target_display = _get_target_display()
var target_rect: Rect2i = DisplayServer.screen_get_usable_rect(target_display)
window_position = Vector2i(-1, -1) # Mark for centering
display_index = target_display
return _center_window_on_screen(target_rect)
# Update embed setting for future comparisons
update_embed_setting()
# Use default centering if position is not set
if window_position == Vector2i(-1, -1):
var target_display = _get_target_display()
var target_rect: Rect2i = DisplayServer.screen_get_usable_rect(target_display)
return _center_window_on_screen(target_rect)
# If we have a specific display preference, validate against it first
if display_index >= 0 and display_index < DisplayServer.get_screen_count():
var preferred_rect: Rect2i = DisplayServer.screen_get_usable_rect(display_index)
var validated_pos = _clamp_window_to_screen(window_position, preferred_rect)
if validated_pos != window_position:
Loggie.msg("Window position adjusted to fit display %d" % display_index).domain(LoggieConsoleConstants.DOMAIN).warn()
return validated_pos
# Check if window can fit properly on any connected display
var screens: int = DisplayServer.get_screen_count()
var best_display: int = -1
var best_pos: Vector2i = window_position
for i: int in screens:
var screen_rect: Rect2i = DisplayServer.screen_get_usable_rect(i)
var window_rect: Rect2i = Rect2i(window_position, window_size)
# Check if window intersects with this screen
if screen_rect.intersects(window_rect):
best_display = i
best_pos = _clamp_window_to_screen(window_position, screen_rect)
# If the window fits perfectly on this screen, use it
if best_pos == window_position:
display_index = i
return window_position
break
# If we found a suitable display, use the clamped position
if best_display >= 0:
display_index = best_display
if best_pos != window_position:
Loggie.msg("Window position clamped to fit display %d" % best_display).domain(LoggieConsoleConstants.DOMAIN).warn()
return best_pos
# Window is completely off-screen, center it on target display
var target_display = _get_target_display()
var target_rect: Rect2i = DisplayServer.screen_get_usable_rect(target_display)
display_index = target_display
Loggie.msg("Window position off-screen, centering on display %d" % target_display).domain(LoggieConsoleConstants.DOMAIN).warn()
return _center_window_on_screen(target_rect)
## Centers the window on the given screen rectangle with margins
##
## Calculates a centered position that keeps the window fully visible with
## appropriate margins from screen edges.
## @param screen_rect: The target screen's usable rectangle
## @return: Position that centers the window with 20px margins
func _center_window_on_screen(screen_rect: Rect2i) -> Vector2i:
var margin_vector: Vector2i = Vector2i(WINDOW_EDGE_MARGIN, WINDOW_EDGE_MARGIN)
var available_size: Vector2i = screen_rect.size - margin_vector * 2
var centered_offset: Vector2i = (available_size - window_size) / 2
return screen_rect.position + margin_vector + centered_offset
## Clamps window position to ensure it fits entirely within screen bounds
##
## Adjusts the position to keep the entire window visible on the screen,
## respecting minimum margins from edges.
## @param pos: The desired window position
## @param screen_rect: The target screen's rectangle
## @return: Adjusted position that keeps the entire window visible
func _clamp_window_to_screen(pos: Vector2i, screen_rect: Rect2i) -> Vector2i:
var margin: Vector2i = Vector2i(WINDOW_MIN_MARGIN, WINDOW_MIN_MARGIN)
var usable_rect: Rect2i = Rect2i(
screen_rect.position + margin,
screen_rect.size - margin * 2
)
# Ensure window fits within usable area
var max_pos: Vector2i = usable_rect.position + usable_rect.size - window_size
var clamped_pos: Vector2i = Vector2i(
clampi(pos.x, usable_rect.position.x, max_pos.x),
clampi(pos.y, usable_rect.position.y, max_pos.y)
)
return clamped_pos
## Gets the target display index for window positioning
## Returns: Valid display index (0 if saved display no longer exists)
func _get_target_display() -> int:
# Use saved display if it still exists
if display_index >= 0 and display_index < DisplayServer.get_screen_count():
return display_index
# Fall back to primary display (index 0)
return 0
## Gets the log level as a proper enum value
## Returns: The current log level as LoggieEnums.LogLevel
func get_log_level() -> LoggieEnums.LogLevel:
return log_level as LoggieEnums.LogLevel
## Sets the log level from an enum value
## @param level: The new log level to store
func set_log_level(level: LoggieEnums.LogLevel) -> void:
log_level = level as int
@@ -0,0 +1 @@
uid://w2lnu7heu47v
+563
View File
@@ -0,0 +1,563 @@
## Enhanced LoggieConsole with persistence, minimize/restore, and multi-display support
##
## This console window provides an advanced debugging interface for the Loggie logging system.
## Features include:
## - Persistent window positioning and settings across sessions
## - Multi-display awareness for non-embedded subwindows
## - Domain-based filtering with dynamic discovery
## - Minimize/restore functionality with floating restore button
## - Complete component-based architecture for extensibility
##
## The console automatically saves all settings when closed or minimized, and restores
## the exact previous state on next startup, including window position, size, filters,
## and discovered domains.
class_name LoggieConsole extends Window
# Constants
const LoggieConsoleChannel = preload("res://addons/loggie-console/scripts/loggie_console_channel.gd")
const ConsoleSettingsManager = preload("res://addons/loggie-console/scripts/managers/console_settings_manager.gd")
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
const RestoreButtonScene := preload("res://addons/loggie-console/scenes/restore_button.tscn")
const DomainColorManager = preload("res://addons/loggie-console/scripts/domain_color_manager.gd")
# UI Constants
const WINDOW_MARGIN_PIXELS: int = 5
# Display detection constants
const DEFAULT_DOMAIN_OFFSET: int = 1 # +1 for default domain in total domain count
# Exported component references - automatically assigned from scene
@export var buffer: LogBuffer ## Log message display and filtering component
@export var output_level: LogLevelFilter ## Log level selection component
@export var domain_filter: PanelDomainSelector ## Domain multi-selection component
@export var controls: ConsoleControls ## Control buttons and settings component
@export var status_display: StatusDisplay ## Status and statistics display component
# Settings UI references
var _settings_button: Button ## Settings button in controls
var _settings_panel: LoggieConsoleSettingsPanel ## Settings panel component
# Minimize/restore system state
var _restore_button_component: RestoreButton ## Restore button component instance
var _is_minimized: bool = false ## Current minimized state
# Custom Loggie channel for receiving log messages
var _console_channel: LoggieConsoleChannel ## Direct channel integration with Loggie
# Component coordination
var _filter_state: LogBuffer.FilterState ## Shared filter state between components
# Persistence system
var _settings_manager: ConsoleSettingsManager ## Handles save/load operations
var _current_settings: LoggieConsoleSettings ## Current settings instance
# Domain color management
var _domain_color_manager: DomainColorManager ## Centralized color management for all components
## Initializes the console window and all its components
##
## This is the main initialization sequence that:
## 1. Loads persistent settings from disk
## 2. Applies window positioning and visibility settings
## 3. Initializes all components with loaded state
## 4. Sets up the custom Loggie channel for message reception
## 5. Connects all necessary signals for runtime operation
##
## The order is critical - settings must be loaded and applied before
## components are initialized to prevent unwanted signal emissions during startup.
func _ready() -> void:
# Initialize persistence system FIRST
_settings_manager = ConsoleSettingsManager.new()
_current_settings = _settings_manager.load_settings()
_restore_button_component = RestoreButtonScene.instantiate()
_restore_button_component.restore_requested.connect(_on_restore_requested)
# Apply window settings before component initialization
_apply_window_settings()
# Connect window signals for persistence
close_requested.connect(_on_close_requested)
# Initialize components with loaded settings
_initialize_components()
# Setup save triggers after initialization
_setup_save_triggers()
# Setup custom channel
_setup_console_channel()
# Apply loaded filters after components are ready
_apply_loaded_settings()
## Called by our custom LoggieConsoleChannel with complete message data
##
## This is the main message reception callback that receives log messages directly
## from the Loggie system through our custom channel. Unlike signal-based approaches,
## this provides complete type information including LogLevel and MsgType enums.
##
## @param msg: The original LoggieMsg with all metadata
## @param preprocessed_content: Fully formatted message text from Loggie
## @param msg_type: Message type enum (ERROR, WARNING, DEBUG, STANDARD)
## @param log_level: Log level enum (ERROR, WARN, NOTICE, INFO, DEBUG)
func receive_log_message(msg: LoggieMsg, preprocessed_content: String, msg_type: LoggieEnums.MsgType, log_level: LoggieEnums.LogLevel) -> void:
# Check if we need to add a new domain to our list
if not msg.domain_name.is_empty():
domain_filter.add_domain(msg.domain_name)
# Add message to buffer (buffer handles filtering internally)
buffer.add_message(msg, preprocessed_content, msg_type, log_level)
## Sets up the custom Loggie channel for direct message reception
##
## Creates and registers a LoggieConsoleChannel that provides the console with
## direct access to log messages including complete type information. The channel
## is automatically added to the default channels list so it receives all messages
## by default, without requiring explicit channel targeting in log calls.
func _setup_console_channel() -> void:
# Create and register our custom channel
_console_channel = LoggieConsoleChannel.new()
_console_channel.set_console_window(self)
Loggie.add_channel(_console_channel)
# Automatically add our channel to the default channels so console receives all messages
var current_defaults: Array = Loggie.settings.default_channels
if not current_defaults.has("loggie_console"):
var new_defaults: Array = current_defaults.duplicate()
new_defaults.append("loggie_console")
Loggie.settings.default_channels = new_defaults
func _setup_domain_filter() -> void:
# Get all domains dynamically from Loggie
var initial_domains: Array[String] = [""] # Always include default domain
# Get all registered domains from Loggie
for domain_name: String in Loggie.domains.keys():
if not initial_domains.has(domain_name):
initial_domains.append(domain_name)
# Add LoggieConsole internal domain (will appear unselected by default)
if not initial_domains.has(LoggieConsoleConstants.DOMAIN):
var all_domains: Array[String] = initial_domains.duplicate()
all_domains.append(LoggieConsoleConstants.DOMAIN)
# Initialize the domain selector with all domains (including console domain)
domain_filter.set_domains_with_console_unselected(all_domains, initial_domains, [LoggieConsoleConstants.DOMAIN])
else:
# Console domain already exists, use standard initialization
domain_filter.set_domains(initial_domains)
domain_filter.domains_changed.connect(_on_domains_changed)
# Apply initial filter state to buffer (only enabled domains, not internal ones)
_filter_state.enabled_domains = initial_domains
buffer.set_filters(_filter_state)
func _on_domains_changed(enabled_domains: Array[String]) -> void:
# Update filter state and apply to buffer
_filter_state.enabled_domains = enabled_domains
buffer.set_filters(_filter_state)
## New component event handlers
func _on_log_level_changed(new_level: LoggieEnums.LogLevel) -> void:
# Update filter state and apply to buffer
_filter_state.min_log_level = new_level
buffer.set_filters(_filter_state)
func _on_clear_requested() -> void:
# Clear the buffer
buffer.clear_all()
func _on_scroll_follow_changed(enabled: bool) -> void:
# Update buffer scroll following
buffer.scroll_following = enabled
func _on_text_search_changed(search_text: String) -> void:
# Update filter state and apply to buffer
_filter_state.text_search = search_text
buffer.set_filters(_filter_state)
func _on_stack_filter_changed(enabled: bool) -> void:
# Update filter state and apply to buffer
_filter_state.show_stack_only = enabled
buffer.set_filters(_filter_state)
func _on_buffer_filter_changed(visible_count: int, total_count: int) -> void:
# Update status display with current counts
var enabled_domains: Array[String] = domain_filter.get_enabled_domains()
var total_domains: int = enabled_domains.size()
if total_domains == 0:
total_domains = Loggie.domains.keys().size() + DEFAULT_DOMAIN_OFFSET
status_display.update_status(total_count, visible_count, enabled_domains, total_domains)
## Minimize/Restore System
## Minimizes the console window and shows a floating restore button
##
## This hides the console window and creates a small floating "LOG" button in the
## top-right corner of the main viewport. The button allows restoring the console
## to its previous position and size. All settings are saved before minimizing
## to ensure state persistence.
func minimize_console() -> void:
if _is_minimized:
Loggie.msg("Already minimized, ignoring request").domain(LoggieConsoleConstants.DOMAIN).warn()
return
# Save current settings including window state before minimizing
_capture_current_state()
_settings_manager.save_settings(_current_settings)
_is_minimized = true
hide() # Hide the console window
_show_restore_button()
## Creates and shows the restore button component
##
## Instantiates the RestoreButton scene and displays it in the root viewport.
## Connects the restore_requested signal to handle button clicks and restore the console.
func _show_restore_button() -> void:
_restore_button_component.show_restore_button(get_tree().root)
Loggie.msg("RestoreButton show_restore_button() called").domain(LoggieConsoleConstants.DOMAIN).debug()
## Handles restore button click events from the RestoreButton component
##
## Called when the RestoreButton component emits its restore_requested signal.
## Triggers the console restoration process.
func _on_restore_requested() -> void:
restore_console()
## Restores the console window from minimized state
##
## This shows the console window at its previously saved position and size,
## removes the floating restore button, and updates the minimized state.
## The window is restored exactly as it was when minimized.
func restore_console() -> void:
if not _is_minimized:
Loggie.msg("Not minimized, ignoring restore request").domain(LoggieConsoleConstants.DOMAIN).warn()
return
# Restore to saved position and size from settings
position = _current_settings.window_position
size = _current_settings.window_size
_is_minimized = false
show() # Show the console window
_hide_restore_button()
## Hides and cleans up the restore button component
##
## Removes the RestoreButton component from the scene tree and frees its resources.
## Ensures proper cleanup when the console is restored from minimized state.
func _hide_restore_button() -> void:
if _restore_button_component and is_instance_valid(_restore_button_component):
_restore_button_component.hide_restore_button()
Loggie.msg("Restore button component cleaned up").domain(LoggieConsoleConstants.DOMAIN).debug()
## Applies saved window properties to the console window
## Validates position to ensure window remains visible on current display setup
func _apply_window_settings() -> void:
# Check if we're using embedded subwindows
var subwindows_embedded: bool = ProjectSettings.get_setting("display/window/subwindows/embed_subwindows", true)
var validated_pos: Vector2i
if subwindows_embedded and _is_outside_viewport():
validated_pos = _center_window()
else:
# Use existing validation for separate windows or valid embedded positions
validated_pos = _current_settings.validate_window_position()
position = validated_pos
# Apply saved window dimensions
size = _current_settings.window_size
# Apply visibility state
visible = _current_settings.is_visible
# Apply minimized state (defer restore button creation)
_is_minimized = _current_settings.is_minimized
if _is_minimized:
hide()
# Defer restore button creation until scene tree is ready
_show_restore_button.call_deferred()
## Checks if console window is outside the main viewport (for embedded subwindows)
func _is_outside_viewport() -> bool:
var viewport_size: Vector2i = Vector2i(get_tree().root.get_visible_rect().size)
var window_rect: Rect2i = Rect2i(_current_settings.window_position, _current_settings.window_size)
var viewport_rect: Rect2i = Rect2i(Vector2i.ZERO, viewport_size)
# Return true if window is not completely within viewport or has invalid position
return _current_settings.window_position == Vector2i(-1, -1) or not viewport_rect.encloses(window_rect)
## Centers the console window in the main viewport
func _center_window() -> Vector2i:
var viewport_size: Vector2i = Vector2i(get_tree().root.get_visible_rect().size)
const MARGIN = 50
var centered_pos: Vector2i = (viewport_size - _current_settings.window_size) / 2
return Vector2i(maxi(MARGIN, centered_pos.x), maxi(MARGIN, centered_pos.y))
## Initializes all console components with loaded settings before connecting signals
## This prevents unwanted signal emissions during the initialization process
func _initialize_components() -> void:
# Initialize base filter state
_filter_state = LogBuffer.FilterState.new()
# Create domain color manager
_domain_color_manager = DomainColorManager.new()
# Pass color manager to components that need it
buffer.set_color_manager(_domain_color_manager)
domain_filter.set_color_manager(_domain_color_manager)
# Load persistent settings into each component
buffer.load_settings_from_resource(_current_settings)
domain_filter.load_settings_from_resource(_current_settings)
controls.load_settings_from_resource(_current_settings)
output_level.load_settings_from_resource(_current_settings)
# Connect component signals for runtime interaction
output_level.level_changed.connect(_on_log_level_changed)
controls.clear_requested.connect(_on_clear_requested)
controls.scroll_follow_changed.connect(_on_scroll_follow_changed)
controls.text_search_changed.connect(_on_text_search_changed)
controls.stack_filter_changed.connect(_on_stack_filter_changed)
# Configure buffer with loaded scroll following setting
buffer.filter_changed.connect(_on_buffer_filter_changed)
buffer.scroll_following = controls.is_scroll_follow_enabled()
# Set up remaining console systems
_setup_domain_filter()
# Initialize settings UI
_setup_settings_ui()
status_display.reset()
## Finalizes the loading process by applying loaded filter settings
## Called after component initialization to ensure filters work with existing messages
func _apply_loaded_settings() -> void:
# Apply the loaded filter state to any existing messages
buffer.apply_loaded_filters()
# Sync domain filter state to ensure consistency
domain_filter.domains_changed.emit(domain_filter.get_enabled_domains())
# Apply text size setting to theme
_apply_text_size_to_theme(_current_settings.text_size_multiplier)
# Apply restore button alignment
_apply_restore_button_alignment(_current_settings.restore_button_alignment)
## Placeholder for save triggers (not used in exit-tree persistence model)
func _setup_save_triggers() -> void:
pass # Settings saved only on exit tree
## Captures current state from window and all components into the settings resource
## This is called before each save operation to ensure all current state is preserved
func _capture_current_state() -> void:
# Capture window properties
_current_settings.window_position = position
_current_settings.window_size = size
_current_settings.is_visible = visible
_current_settings.is_minimized = _is_minimized
# Capture current display index (important when embed_subwindows=false)
_current_settings.display_index = _get_current_display_index()
# Capture current embed_subwindows setting for change detection
_current_settings.update_embed_setting()
# Let each component save its own state
buffer.save_settings_to_resource(_current_settings)
domain_filter.save_settings_to_resource(_current_settings)
controls.save_settings_to_resource(_current_settings)
output_level.save_settings_to_resource(_current_settings)
## Manually saves current console settings to disk
## Useful for forcing a save before potentially destructive operations
func save_settings() -> void:
_capture_current_state()
_settings_manager.save_settings(_current_settings)
## Handles window close requests by saving settings first, then minimizing
## This ensures settings are preserved even if the console is closed unexpectedly
func _on_close_requested() -> void:
# Preserve settings before minimizing
_capture_current_state()
_settings_manager.save_settings(_current_settings)
# Continue with normal minimize behavior
minimize_console()
## Automatically saves settings when console exits the scene tree
## This is the primary save mechanism ensuring settings persist between sessions
func _exit_tree() -> void:
if _settings_manager and _current_settings:
_capture_current_state()
_settings_manager.save_settings(_current_settings)
## Handles system-level window destruction notifications
## Provides additional safety net for settings preservation
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
if _settings_manager and _current_settings:
_capture_current_state()
_settings_manager.save_settings(_current_settings)
## Determines which display the console window is currently on
## Returns: Display index (0-based) where the window center is located
func _get_current_display_index() -> int:
# Calculate window center point
var window_center: Vector2i = position + size / 2
# Check which display contains the window center
var screens: int = DisplayServer.get_screen_count()
for i: int in screens:
var screen_rect: Rect2i = DisplayServer.screen_get_usable_rect(i)
if screen_rect.has_point(window_center):
return i
# Fallback: find display with most window overlap
var max_overlap: int = 0
var best_display: int = 0
var window_rect: Rect2i = Rect2i(position, size)
for i: int in screens:
var screen_rect: Rect2i = DisplayServer.screen_get_usable_rect(i)
var intersection: Rect2i = screen_rect.intersection(window_rect)
var overlap_area: int = intersection.size.x * intersection.size.y
if overlap_area > max_overlap:
max_overlap = overlap_area
best_display = i
return best_display
## Sets up the settings UI components and connects their signals
##
## Initializes the settings button and panel component, loads current settings,
## and connects the component's signals for handling settings changes.
func _setup_settings_ui() -> void:
# Get UI component references
_settings_button = get_node("PanelContainer/VBoxContainer/Controls/Settings")
_settings_panel = get_node("SettingsPanel")
# Connect signals
_settings_button.pressed.connect(_on_settings_button_pressed)
_settings_panel.settings_applied.connect(_on_settings_applied)
_settings_panel.settings_reset.connect(_on_settings_reset)
# Load current settings into the panel
_settings_panel.set_text_size_multiplier(_current_settings.text_size_multiplier)
_settings_panel.set_restore_button_alignment(_current_settings.restore_button_alignment)
## Called when the Settings button is pressed
##
## Shows the settings panel dialog with current values loaded.
func _on_settings_button_pressed() -> void:
_settings_panel.show_settings()
## Called when settings are applied via the settings panel
##
## Saves the new settings and applies them dynamically.
## @param text_size_multiplier: The new text size multiplier value
## @param restore_button_alignment: The new restore button alignment
func _on_settings_applied(text_size_multiplier: float, restore_button_alignment: RestoreButton.Alignment) -> void:
# Update settings
_current_settings.text_size_multiplier = text_size_multiplier
_current_settings.restore_button_alignment = restore_button_alignment as int
_settings_manager.save_settings(_current_settings)
# Apply text size change to theme
_apply_text_size_to_theme(text_size_multiplier)
# Apply restore button alignment
_apply_restore_button_alignment(restore_button_alignment as int)
Loggie.msg("Settings updated: text size %.1fx, alignment %s" % [text_size_multiplier, _get_alignment_name(restore_button_alignment)]).domain(LoggieConsoleConstants.DOMAIN).info()
## Called when the settings are reset to defaults
##
## Logs the reset action for user feedback.
func _on_settings_reset() -> void:
Loggie.msg("Settings reset to defaults").domain(LoggieConsoleConstants.DOMAIN).info()
## Dynamically applies text size multiplier to the current theme
##
## Modifies font sizes in the theme resource at runtime to immediately
## reflect the new text size setting without requiring a restart.
## @param multiplier: Text size multiplier (0.5 to 2.0)
func _apply_text_size_to_theme(multiplier: float) -> void:
var console_theme: Theme = get_theme()
if not console_theme:
Loggie.msg("No theme found, cannot apply text size").domain(LoggieConsoleConstants.DOMAIN).error()
return
# Define base font sizes (from theme_compact.tres)
var base_font_sizes: Dictionary = {
"Button/font_size": 11,
"CheckBox/font_size": 11,
"LineEdit/font_size": 12,
"MenuButton/font_size": 11,
"OptionButton/font_size": 11,
"PopupMenu/font_size": 11,
"RichTextLabel/bold_font_size": 10,
"RichTextLabel/bold_italics_font_size": 10,
"RichTextLabel/italics_font_size": 10,
"RichTextLabel/mono_font_size": 9,
"RichTextLabel/normal_font_size": 10,
"Window/title_font_size": 16
}
# Apply multiplier to each font size
for font_path: String in base_font_sizes:
var parts: PackedStringArray = font_path.split("/")
var control_type: String = parts[0]
var property: String = parts[1]
var base_size: int = base_font_sizes[font_path]
var new_size: int = int(base_size * multiplier)
console_theme.set_font_size(property, control_type, new_size)
# Theme changes are applied automatically
## Applies the restore button alignment setting
##
## Updates the restore button position based on the alignment preference.
## @param alignment: Alignment enum value (0=TOP_LEFT, 1=TOP_RIGHT, 2=BOTTOM_LEFT, 3=BOTTOM_RIGHT)
func _apply_restore_button_alignment(alignment: int) -> void:
if not _restore_button_component:
return
# Set position based on alignment using the RestoreButton's enum
var restore_alignment: RestoreButton.Alignment = alignment as RestoreButton.Alignment
_restore_button_component.set_alignment(restore_alignment)
## Gets a human-readable name for the alignment enum
##
## @param alignment: The alignment enum value
## @return: String name of the alignment
func _get_alignment_name(alignment: RestoreButton.Alignment) -> String:
match alignment:
RestoreButton.Alignment.TOP_LEFT:
return "Top Left"
RestoreButton.Alignment.TOP_RIGHT:
return "Top Right"
RestoreButton.Alignment.BOTTOM_LEFT:
return "Bottom Left"
RestoreButton.Alignment.BOTTOM_RIGHT:
return "Bottom Right"
_:
return "Unknown"
@@ -0,0 +1 @@
uid://64r2in67soep
+164
View File
@@ -0,0 +1,164 @@
[gd_scene format=3 uid="uid://d0sxs0is80byx"]
[ext_resource type="Script" uid="uid://64r2in67soep" path="res://addons/loggie-console/scenes/console.gd" id="1_yp485"]
[ext_resource type="PackedScene" uid="uid://dgok5l3frx53m" path="res://addons/loggie-console/scenes/panel_domain_selector.tscn" id="2_domain"]
[ext_resource type="StyleBox" uid="uid://b7k8yo7jfhpue" path="res://addons/loggie-console/assets/inner_panel.tres" id="3_fxnpl"]
[ext_resource type="Script" uid="uid://wah8efffydnu" path="res://addons/loggie-console/scripts/components/log_buffer.gd" id="3_logbuffer"]
[ext_resource type="Theme" uid="uid://c1lnnysnspc6v" path="res://addons/loggie-console/theme_compact.tres" id="4_2cgp1"]
[ext_resource type="Script" uid="uid://d3l2tmf5gym1d" path="res://addons/loggie-console/scripts/components/log_level_filter.gd" id="4_loglevel"]
[ext_resource type="Script" uid="uid://c4f6c3gs8venj" path="res://addons/loggie-console/scripts/components/console_controls.gd" id="5_controls"]
[ext_resource type="Script" uid="uid://whrim6lbiako" path="res://addons/loggie-console/scripts/components/status_display.gd" id="6_status"]
[ext_resource type="Texture2D" uid="uid://ivppb5cqrsak" path="res://addons/loggie-console/assets/NinePatchWindow-InnerRecess.png" id="7_56hvo"]
[ext_resource type="PackedScene" uid="uid://bw8k3yap2n4qv" path="res://addons/loggie-console/scenes/settings_panel.tscn" id="7_settings_panel"]
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_20rq3"]
content_margin_left = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
texture = ExtResource("7_56hvo")
texture_margin_left = 2.0
texture_margin_top = 2.0
texture_margin_right = 2.0
texture_margin_bottom = 2.0
axis_stretch_horizontal = 1
axis_stretch_vertical = 1
region_rect = Rect2(0, 0, 36, 36)
modulate_color = Color(0.60933, 0.60933, 0.60933, 1)
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_njst7"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ibivl"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_0yrjc"]
[node name="LoggieConsoleWindow" type="Window" unique_id=1024795111 node_paths=PackedStringArray("buffer", "output_level", "domain_filter", "controls", "status_display")]
oversampling_override = 1.0
title = "Loggie Console"
initial_position = 1
size = Vector2i(889, 905)
wrap_controls = true
sharp_corners = true
min_size = Vector2i(600, 300)
keep_title_visible = true
theme = ExtResource("4_2cgp1")
script = ExtResource("1_yp485")
buffer = NodePath("PanelContainer/VBoxContainer/PanelContainer/Buffer")
output_level = NodePath("PanelContainer/VBoxContainer/Controls/OutputLevel")
domain_filter = NodePath("PanelContainer/VBoxContainer/Controls/DomainFilter")
controls = NodePath("PanelContainer/VBoxContainer/Controls")
status_display = NodePath("PanelContainer/VBoxContainer/StatusContainer/HBoxContainer/StatusLabel")
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=1949067581]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = ExtResource("3_fxnpl")
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer" unique_id=1611268808]
layout_mode = 2
[node name="Controls" type="HBoxContainer" parent="PanelContainer/VBoxContainer" unique_id=251197975 node_paths=PackedStringArray("clear_button", "scroll_follow_checkbox", "search_input", "stack_filter_checkbox")]
layout_mode = 2
script = ExtResource("5_controls")
clear_button = NodePath("Clear")
scroll_follow_checkbox = NodePath("ScrollFollow")
search_input = NodePath("../StatusContainer/HBoxContainer/SearchInput")
stack_filter_checkbox = NodePath("StackFilter")
[node name="OutputLevel" type="OptionButton" parent="PanelContainer/VBoxContainer/Controls" unique_id=751332603]
custom_minimum_size = Vector2(60, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Filter by log level"
selected = 3
item_count = 5
popup/item_0/text = "ERROR"
popup/item_0/id = 0
popup/item_1/text = "WARN"
popup/item_1/id = 1
popup/item_2/text = "NOTICE"
popup/item_2/id = 2
popup/item_3/text = "INFO"
popup/item_3/id = 3
popup/item_4/text = "DEBUG"
popup/item_4/id = 4
script = ExtResource("4_loglevel")
[node name="ScrollFollow" type="CheckBox" parent="PanelContainer/VBoxContainer/Controls" unique_id=569696074]
custom_minimum_size = Vector2(50, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Auto-scroll to newest messages"
button_pressed = true
text = "Follow"
[node name="StackFilter" type="CheckBox" parent="PanelContainer/VBoxContainer/Controls" unique_id=719019705]
custom_minimum_size = Vector2(80, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Show only messages with stack traces"
text = "Stack Only"
[node name="Clear" type="Button" parent="PanelContainer/VBoxContainer/Controls" unique_id=1133245115]
custom_minimum_size = Vector2(45, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Clear console buffer"
text = "Clear"
[node name="Settings" type="Button" parent="PanelContainer/VBoxContainer/Controls" unique_id=671578664]
custom_minimum_size = Vector2(60, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Console settings"
text = "Settings"
[node name="DomainFilter" parent="PanelContainer/VBoxContainer/Controls" unique_id=582666238 instance=ExtResource("2_domain")]
custom_minimum_size = Vector2(300, 20)
layout_mode = 2
size_flags_horizontal = 10
[node name="PanelContainer" type="PanelContainer" parent="PanelContainer/VBoxContainer" unique_id=2068430600]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxTexture_20rq3")
[node name="Buffer" type="RichTextLabel" parent="PanelContainer/VBoxContainer/PanelContainer" unique_id=1261116881]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_njst7")
theme_override_styles/focus = SubResource("StyleBoxEmpty_ibivl")
bbcode_enabled = true
scroll_following = true
context_menu_enabled = true
selection_enabled = true
script = ExtResource("3_logbuffer")
[node name="StatusContainer" type="PanelContainer" parent="PanelContainer/VBoxContainer" unique_id=592277225]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxEmpty_0yrjc")
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/StatusContainer" unique_id=958031751]
layout_mode = 2
[node name="SearchInput" type="LineEdit" parent="PanelContainer/VBoxContainer/StatusContainer/HBoxContainer" unique_id=526298685]
custom_minimum_size = Vector2(200, 20)
layout_mode = 2
size_flags_horizontal = 2
placeholder_text = "Filter messages..."
clear_button_enabled = true
[node name="StatusLabel" type="Label" parent="PanelContainer/VBoxContainer/StatusContainer/HBoxContainer" unique_id=848734114]
layout_mode = 2
size_flags_horizontal = 8
theme_override_font_sizes/font_size = 12
text = "Ready"
horizontal_alignment = 2
vertical_alignment = 1
script = ExtResource("6_status")
[node name="SettingsPanel" parent="." unique_id=1146821245 instance=ExtResource("7_settings_panel")]
@@ -0,0 +1,55 @@
[gd_scene load_steps=3 format=3 uid="uid://cjy8b6h1xrq2x"]
[ext_resource type="Script" uid="uid://dgkxvw0v5n6hq" path="res://addons/loggie-console/scripts/components/domain_item.gd" id="1_domain_item"]
[ext_resource type="Theme" uid="uid://c1lnnysnspc6v" path="res://addons/loggie-console/theme_compact.tres" id="2_theme"]
[node name="DomainItem" type="Control" node_paths=PackedStringArray("select_checkbox", "only_button", "color_indicator")]
custom_minimum_size = Vector2(280, 32)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_domain_item")
select_checkbox = NodePath("HBoxContainer/SelectCheckBox")
only_button = NodePath("OnlyButton")
color_indicator = NodePath("HBoxContainer/ColorIndicator")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ColorIndicator" type="ColorRect" parent="HBoxContainer"]
custom_minimum_size = Vector2(12, 12)
layout_mode = 2
[node name="SelectCheckBox" type="CheckBox" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Select/deselect this domain"
theme_override_constants/h_separation = 20
text_overrun_behavior = 3
[node name="OnlyButton" type="Button" parent="."]
modulate = Color(1, 1, 1, 0)
custom_minimum_size = Vector2(45, 24)
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -45.0
offset_top = -12.0
offset_bottom = 12.0
grow_horizontal = 0
grow_vertical = 2
tooltip_text = "Select only this domain"
mouse_filter = 2
text = "Only"
@@ -0,0 +1,86 @@
[gd_scene load_steps=4 format=3 uid="uid://dgok5l3frx53m"]
[ext_resource type="Script" uid="uid://bfmpjs4x3fhte" path="res://addons/loggie-console/scripts/components/panel_domain_selector.gd" id="1_panel_selector"]
[ext_resource type="Theme" uid="uid://c1lnnysnspc6v" path="res://addons/loggie-console/theme_compact.tres" id="2_theme"]
[ext_resource type="StyleBox" uid="uid://b7k8yo7jfhpue" path="res://addons/loggie-console/assets/inner_panel.tres" id="3_panel_style"]
[node name="PanelDomainSelector" type="Control" node_paths=PackedStringArray("trigger_button", "domain_panel", "select_all_button", "select_none_button", "scroll_container", "domain_list")]
custom_minimum_size = Vector2(100, 20)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2_theme")
script = ExtResource("1_panel_selector")
trigger_button = NodePath("TriggerButton")
domain_panel = NodePath("DomainPanel")
select_all_button = NodePath("DomainPanel/PanelContainer/VBoxContainer/SelectionControls/SelectAllButton")
select_none_button = NodePath("DomainPanel/PanelContainer/VBoxContainer/SelectionControls/SelectNoneButton")
scroll_container = NodePath("DomainPanel/PanelContainer/VBoxContainer/ScrollContainer")
domain_list = NodePath("DomainPanel/PanelContainer/VBoxContainer/ScrollContainer/DomainList")
[node name="TriggerButton" type="Button" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
tooltip_text = "All domains selected (1)"
text = "Domains: All"
text_overrun_behavior = 3
[node name="DomainPanel" type="PopupPanel" parent="."]
initial_position = 1
size = Vector2i(300, 160)
visible = true
min_size = Vector2i(300, 150)
[node name="PanelContainer" type="PanelContainer" parent="DomainPanel"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = ExtResource("3_panel_style")
[node name="VBoxContainer" type="VBoxContainer" parent="DomainPanel/PanelContainer"]
layout_mode = 2
[node name="SelectionControls" type="HBoxContainer" parent="DomainPanel/PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 0
alignment = 2
[node name="SelectAllButton" type="Button" parent="DomainPanel/PanelContainer/VBoxContainer/SelectionControls"]
custom_minimum_size = Vector2(60, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Select all domains"
text = "Select All"
[node name="SelectNoneButton" type="Button" parent="DomainPanel/PanelContainer/VBoxContainer/SelectionControls"]
custom_minimum_size = Vector2(60, 20)
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Deselect all domains"
text = "None"
[node name="HSeparator" type="HSeparator" parent="DomainPanel/PanelContainer/VBoxContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="DomainPanel/PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="DomainList" type="VBoxContainer" parent="DomainPanel/PanelContainer/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
@@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://cb5mkvrnm3hxn"]
[ext_resource type="Script" uid="uid://dmyd1gquxy42u" path="res://addons/loggie-console/scripts/components/restore_button.gd" id="1_2h8k5"]
[ext_resource type="Theme" uid="uid://c1lnnysnspc6v" path="res://addons/loggie-console/theme_compact.tres" id="1_o01pp"]
[node name="LoggieConsoleOverlay" type="CanvasLayer" node_paths=PackedStringArray("restore_button")]
layer = 2147483647
script = ExtResource("1_2h8k5")
restore_button = NodePath("MarginContainer/Button")
[node name="MarginContainer" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="Button" type="Button" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 8
size_flags_vertical = 0
tooltip_text = "Show Loggie Console"
theme = ExtResource("1_o01pp")
text = " Loggie Console "
@@ -0,0 +1,90 @@
[gd_scene load_steps=4 format=3 uid="uid://bw8k3yap2n4qv"]
[ext_resource type="Theme" uid="uid://c1lnnysnspc6v" path="res://addons/loggie-console/theme_compact.tres" id="1_08jdj"]
[ext_resource type="Script" uid="uid://giuyu6vhhgam" path="res://addons/loggie-console/scripts/components/settings_panel.gd" id="1_settings_panel"]
[ext_resource type="StyleBox" uid="uid://b7k8yo7jfhpue" path="res://addons/loggie-console/assets/inner_panel.tres" id="2_08jdj"]
[node name="SettingsPanel" type="AcceptDialog" node_paths=PackedStringArray("_text_size_slider", "_text_size_value_label", "_reset_button", "_alignment_option")]
title = "Console Settings"
initial_position = 1
size = Vector2i(320, 210)
transient = false
sharp_corners = true
theme = ExtResource("1_08jdj")
theme_override_styles/panel = ExtResource("2_08jdj")
ok_button_text = " Apply Changes "
script = ExtResource("1_settings_panel")
_text_size_slider = NodePath("VBoxContainer/TextSizeContainer/TextSizeSlider")
_text_size_value_label = NodePath("VBoxContainer/TextSizeContainer/TextSizeValueLabel")
_reset_button = NodePath("VBoxContainer/ResetContainer/ResetButton")
_alignment_option = NodePath("VBoxContainer/AlignmentContainer/AlignmentOption")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -38.0
grow_horizontal = 2
grow_vertical = 2
[node name="TextSizeContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="TextSizeLabel" type="Label" parent="VBoxContainer/TextSizeContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Text Size:"
vertical_alignment = 1
[node name="TextSizeSlider" type="HSlider" parent="VBoxContainer/TextSizeContainer"]
layout_mode = 2
size_flags_horizontal = 3
min_value = 0.5
max_value = 2.0
step = 0.1
value = 1.0
tick_count = 16
ticks_on_borders = true
[node name="TextSizeValueLabel" type="Label" parent="VBoxContainer/TextSizeContainer"]
custom_minimum_size = Vector2(40, 0)
layout_mode = 2
size_flags_horizontal = 0
text = "1.0x"
horizontal_alignment = 2
vertical_alignment = 1
[node name="AlignmentContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="AlignmentLabel" type="Label" parent="VBoxContainer/AlignmentContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Restore Button:"
vertical_alignment = 1
[node name="AlignmentOption" type="OptionButton" parent="VBoxContainer/AlignmentContainer"]
layout_mode = 2
size_flags_horizontal = 3
selected = 1
item_count = 4
popup/item_0/text = "Top Left"
popup/item_0/id = 0
popup/item_1/text = "Top Right"
popup/item_1/id = 1
popup/item_2/text = "Bottom Left"
popup/item_2/id = 2
popup/item_3/text = "Bottom Right"
popup/item_3/id = 3
[node name="ResetContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="ResetButton" type="Button" parent="VBoxContainer/ResetContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Reset to Default"
@@ -0,0 +1,104 @@
class_name ConsoleControls extends HBoxContainer
## Console controls toolbar component
## Manages clear button, scroll follow, and other control elements
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
# Signals
signal clear_requested()
signal scroll_follow_changed(enabled: bool)
signal text_search_changed(search_text: String)
signal stack_filter_changed(enabled: bool)
# Exported references to child controls
@export var clear_button: Button
@export var scroll_follow_checkbox: CheckBox
@export var search_input: LineEdit
@export var stack_filter_checkbox: CheckBox
func _ready() -> void:
_setup_controls()
## Initialize the controls with default states
func initialize() -> void:
if scroll_follow_checkbox:
scroll_follow_checkbox.button_pressed = true
_on_scroll_follow_toggled(true)
## Get current scroll follow state
func is_scroll_follow_enabled() -> bool:
return scroll_follow_checkbox.button_pressed if scroll_follow_checkbox else false
## Set scroll follow state programmatically
func set_scroll_follow(enabled: bool) -> void:
if scroll_follow_checkbox:
scroll_follow_checkbox.button_pressed = enabled
## Get current text search
func get_search_text() -> String:
return search_input.text if search_input else ""
## Set search text programmatically
func set_search_text(text: String) -> void:
if search_input:
search_input.text = text
## Get current stack filter state
func is_stack_filter_enabled() -> bool:
return stack_filter_checkbox.button_pressed if stack_filter_checkbox else false
## Set stack filter state programmatically
func set_stack_filter(enabled: bool) -> void:
if stack_filter_checkbox:
stack_filter_checkbox.button_pressed = enabled
## Private methods
func _setup_controls() -> void:
# Connect clear button if available
if clear_button:
clear_button.pressed.connect(_on_clear_pressed)
clear_button.tooltip_text = "Clear console buffer"
# Connect scroll follow checkbox if available
if scroll_follow_checkbox:
scroll_follow_checkbox.toggled.connect(_on_scroll_follow_toggled)
scroll_follow_checkbox.tooltip_text = "Auto-scroll to newest messages"
# Connect search input if available
if search_input:
search_input.text_changed.connect(_on_search_text_changed)
search_input.placeholder_text = "Filter messages..."
# Connect stack filter checkbox if available
if stack_filter_checkbox:
stack_filter_checkbox.toggled.connect(_on_stack_filter_toggled)
stack_filter_checkbox.tooltip_text = "Show only messages with stack traces"
func _on_clear_pressed() -> void:
clear_requested.emit()
func _on_scroll_follow_toggled(enabled: bool) -> void:
scroll_follow_changed.emit(enabled)
func _on_search_text_changed(new_text: String) -> void:
text_search_changed.emit(new_text)
func _on_stack_filter_toggled(enabled: bool) -> void:
stack_filter_changed.emit(enabled)
## Saves current toolbar control states to persistent settings
## Param settings: The settings resource to populate with current control states
func save_settings_to_resource(settings: LoggieConsoleSettings) -> void:
settings.scroll_follow_enabled = is_scroll_follow_enabled()
settings.search_text = get_search_text()
settings.stack_filter_enabled = is_stack_filter_enabled()
## Loads toolbar control states from persistent settings and applies them
## Updates all UI controls to reflect the loaded state without emitting signals
## Param settings: The settings resource containing saved control states
func load_settings_from_resource(settings: LoggieConsoleSettings) -> void:
set_scroll_follow(settings.scroll_follow_enabled)
set_search_text(settings.search_text)
set_stack_filter(settings.stack_filter_enabled)
@@ -0,0 +1 @@
uid://c4f6c3gs8venj
@@ -0,0 +1,166 @@
class_name DomainItem extends Control
## Custom control for individual domain rows in PanelDomainSelector
## Provides checkbox selection, domain label, and hover-revealed "Only" button
const DomainColorManager = preload("res://addons/loggie-console/scripts/domain_color_manager.gd")
# Signals
signal selection_changed(domain_name: String, is_selected: bool)
signal only_button_pressed(domain_name: String)
# UI Components - exported for scene tree assignment
@export var select_checkbox: CheckBox
@export var only_button: Button
@export var color_indicator: ColorRect
# Domain state
var _domain_name: String = ""
var _is_hovered: bool = false
var _color_manager: DomainColorManager
# UI Layout constants
const ITEM_MIN_WIDTH: int = 280
const ITEM_MIN_HEIGHT: int = 32
# Visibility constants for only button hover effect
const ONLY_BUTTON_ALPHA_HIDDEN: float = 0.0
const ONLY_BUTTON_ALPHA_VISIBLE: float = 1.0
## Initializes the domain item control with UI connections and hover behavior
##
## Sets up consistent sizing, signal connections for checkbox and button interactions,
## and configures mouse hover detection for the "Only" button visibility toggle.
## The only button starts hidden and becomes visible on hover with smooth alpha transition.
## Requires select_checkbox and only_button to be properly assigned via @export.
func _ready() -> void:
if not select_checkbox:
return
# Set minimum size for consistent layout
custom_minimum_size = Vector2(ITEM_MIN_WIDTH, ITEM_MIN_HEIGHT)
# Connect signals
select_checkbox.toggled.connect(_on_checkbox_toggled)
only_button.pressed.connect(_on_only_button_pressed)
# Setup hover detection on checkbox (which now contains the text)
select_checkbox.mouse_entered.connect(_on_mouse_entered)
select_checkbox.mouse_exited.connect(_on_mouse_exited)
# Also setup hover detection on the main control for complete coverage
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
# Initialize only button as hidden
only_button.modulate.a = ONLY_BUTTON_ALPHA_HIDDEN
only_button.mouse_filter = Control.MOUSE_FILTER_IGNORE
## Set the domain color manager instance
func set_color_manager(color_manager: DomainColorManager) -> void:
_color_manager = color_manager
## Configures the domain item with a specific domain name and selection state
##
## Sets up the checkbox display text (shows "(default)" for empty domain names),
## configures tooltips for both checkbox and only button, and applies the initial
## selection state. This method should be called after the item is added to the scene tree.
##
## @param domain_name: The domain name to display and track
## @param is_selected: Whether the domain should start as selected
func setup_domain(domain_name: String, is_selected: bool = false) -> void:
_domain_name = domain_name
if select_checkbox:
var display_name = domain_name if not domain_name.is_empty() else "(default)"
# Update color indicator instead of modulating checkbox
if _color_manager and color_indicator:
var domain_color = _color_manager.get_domain_color(domain_name)
color_indicator.color = domain_color
select_checkbox.text = display_name
select_checkbox.tooltip_text = "Select/deselect domain: " + domain_name
select_checkbox.button_pressed = is_selected
if only_button:
only_button.text = "Only"
only_button.tooltip_text = "Select only this domain: " + domain_name
## Get the domain name for this item
func get_domain_name() -> String:
return _domain_name
## Updates the checkbox selection state without emitting signals
##
## Useful for programmatically setting the selection state during bulk operations
## like "Select All" or when loading from saved settings. Uses set_pressed_no_signal
## to avoid triggering selection_changed signals during these operations.
##
## @param is_selected: The new selection state for the checkbox
func set_selected(is_selected: bool) -> void:
if select_checkbox:
select_checkbox.set_pressed_no_signal(is_selected)
## Get current selection state
func is_selected() -> bool:
return select_checkbox.button_pressed if select_checkbox else false
## Private methods
func _on_checkbox_toggled(pressed: bool) -> void:
selection_changed.emit(_domain_name, pressed)
func _on_only_button_pressed() -> void:
only_button_pressed.emit(_domain_name)
func _on_mouse_entered() -> void:
_is_hovered = true
_set_only_button_visibility(true)
func _on_mouse_exited() -> void:
_is_hovered = false
_set_only_button_visibility(false)
## Controls the visibility and interaction of the "Only" button based on hover state
##
## Instantly toggles the button's alpha transparency and mouse filter to create
## a hover-reveal effect. When hidden, the button becomes completely transparent
## and ignores mouse input. When visible, it becomes fully opaque and interactive.
##
## @param show: Whether to show (true) or hide (false) the only button
func _set_only_button_visibility(show: bool) -> void:
if not only_button:
return
# Set visibility instantly - no animation
var target_alpha = ONLY_BUTTON_ALPHA_VISIBLE if show else ONLY_BUTTON_ALPHA_HIDDEN
var target_mouse_filter = Control.MOUSE_FILTER_PASS if show else Control.MOUSE_FILTER_IGNORE
only_button.modulate.a = target_alpha
only_button.mouse_filter = target_mouse_filter
func _gui_input(event: InputEvent) -> void:
# Handle keyboard navigation
if event is InputEventKey and event.pressed:
match event.keycode:
KEY_SPACE, KEY_ENTER:
if select_checkbox:
select_checkbox.button_pressed = not select_checkbox.button_pressed
_on_checkbox_toggled(select_checkbox.button_pressed)
accept_event()
KEY_O:
if event.ctrl_pressed: # Ctrl+O for "Only"
_on_only_button_pressed()
accept_event()
func _notification(what: int) -> void:
match what:
NOTIFICATION_FOCUS_ENTER:
# Add visual focus indication if needed
pass
NOTIFICATION_FOCUS_EXIT:
# Remove visual focus indication if needed
pass
@@ -0,0 +1 @@
uid://dgkxvw0v5n6hq
@@ -0,0 +1,256 @@
class_name DomainSelector extends MenuButton
## Standalone domain selector component for filtering log messages by domain
## Provides clean interface with proper signal-based communication
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
const DomainColorManager = preload("res://addons/loggie-console/scripts/domain_color_manager.gd")
# Signals
signal domains_changed(enabled_domains: Array[String])
# Properties
var _enabled_domains: Array[String] = []
var _all_domains: Array[String] = []
var _domain_popup: PopupMenu
var _color_manager: DomainColorManager
# Constants
const ALL_ITEM_ID = 0
const SEPARATOR_ID = 1
const DOMAIN_ID_OFFSET = 2
func _ready() -> void:
_domain_popup = get_popup()
_domain_popup.id_pressed.connect(_on_domain_selected)
_initialize_empty_state()
## Set the domain color manager instance
func set_color_manager(color_manager: DomainColorManager) -> void:
_color_manager = color_manager
## Initialize the selector with available domains
func set_domains(domains: Array[String]) -> void:
_all_domains = domains.duplicate()
_enabled_domains = domains.duplicate()
_rebuild_popup()
_update_button_text()
domains_changed.emit(_enabled_domains)
## Initialize the selector with all domains, but only enable selected ones
## Console domains appear normally but are unselected by default
func set_domains_with_console_unselected(all_domains: Array[String], enabled_domains: Array[String], console_domains: Array[String]) -> void:
_all_domains = all_domains.duplicate()
_enabled_domains = enabled_domains.duplicate()
_rebuild_popup_with_console_unselected(console_domains)
_update_button_text()
domains_changed.emit(_enabled_domains)
## Add a new domain to the selector (for dynamic discovery)
func add_domain(domain_name: String) -> void:
if domain_name.is_empty() or _all_domains.has(domain_name):
return
_all_domains.append(domain_name)
_enabled_domains.append(domain_name)
# Add new item to popup with color icon
var display_name = _get_domain_display_name(domain_name)
var color_icon = _create_domain_color_icon(domain_name)
var item_id = _get_domain_item_id(_all_domains.size() - 1)
_domain_popup.add_check_item(display_name, item_id)
_domain_popup.set_item_icon(item_id, color_icon)
_domain_popup.set_item_checked(item_id, true)
_update_all_checkbox()
_update_button_text()
domains_changed.emit(_enabled_domains)
## Get currently enabled domains
func get_enabled_domains() -> Array[String]:
return _enabled_domains.duplicate()
## Check if a domain is enabled
func is_domain_enabled(domain_name: String) -> bool:
return _enabled_domains.has(domain_name)
## Private methods
## Create colored icon for domain display in popup
func _create_domain_color_icon(domain_name: String, size: int = 12) -> ImageTexture:
var color: Color = Color.WHITE # Default color
if _color_manager:
color = _color_manager.get_domain_color(domain_name)
# Create a small colored square image
var image = Image.create(size, size, false, Image.FORMAT_RGBA8)
image.fill(color)
# Convert to texture
var texture: ImageTexture = ImageTexture.create_from_image(image)
return texture
## Get display name for domain (without color formatting)
func _get_domain_display_name(domain_name: String) -> String:
return domain_name if not domain_name.is_empty() else "(default)"
func _initialize_empty_state() -> void:
_domain_popup.clear()
_domain_popup.add_item("No domains available")
_domain_popup.set_item_disabled(0, true)
text = "Domains: None"
func _rebuild_popup() -> void:
_domain_popup.clear()
# Add "All" toggle
_domain_popup.add_check_item("All", ALL_ITEM_ID)
_domain_popup.set_item_checked(ALL_ITEM_ID, _enabled_domains.size() == _all_domains.size())
# Add separator
_domain_popup.add_separator()
# Add individual domain items with color icons
for i in range(_all_domains.size()):
var domain_name = _all_domains[i]
var display_name = _get_domain_display_name(domain_name)
var color_icon = _create_domain_color_icon(domain_name)
var item_id = _get_domain_item_id(i)
_domain_popup.add_check_item(display_name, item_id)
_domain_popup.set_item_icon(item_id, color_icon)
_domain_popup.set_item_checked(item_id, _enabled_domains.has(domain_name))
## Builds popup with console domain unselected by default (no separator)
func _rebuild_popup_with_console_unselected(console_domains: Array[String]) -> void:
_domain_popup.clear()
# Add "All" toggle (only for non-console domains)
_domain_popup.add_check_item("All", ALL_ITEM_ID)
var non_console_domains = []
for domain in _all_domains:
if not console_domains.has(domain):
non_console_domains.append(domain)
_domain_popup.set_item_checked(ALL_ITEM_ID, _enabled_domains.size() == non_console_domains.size())
# Add separator
_domain_popup.add_separator()
# Add all domain items in order with color icons
for i in range(_all_domains.size()):
var domain_name = _all_domains[i]
var display_name: String
# Use friendly name for console domain
if console_domains.has(domain_name):
display_name = "LoggieConsole" # Simplified name for console domain
else:
display_name = domain_name if not domain_name.is_empty() else "(default)"
# Create color icon for the domain
var color_icon = _create_domain_color_icon(domain_name)
var item_id = _get_domain_item_id(i)
_domain_popup.add_check_item(display_name, item_id)
_domain_popup.set_item_icon(item_id, color_icon)
_domain_popup.set_item_checked(item_id, _enabled_domains.has(domain_name))
func _on_domain_selected(id: int) -> void:
if id == ALL_ITEM_ID:
_toggle_all_domains()
else:
_toggle_individual_domain(id)
_update_all_checkbox()
_update_button_text()
domains_changed.emit(_enabled_domains)
func _toggle_all_domains() -> void:
var current_all_state = _domain_popup.is_item_checked(ALL_ITEM_ID)
var new_all_state = not current_all_state
# Update all checkbox
_domain_popup.set_item_checked(ALL_ITEM_ID, new_all_state)
# Update all domain checkboxes and enabled list
_enabled_domains.clear()
for i in range(_all_domains.size()):
var item_id = _get_domain_item_id(i)
_domain_popup.set_item_checked(item_id, new_all_state)
if new_all_state:
_enabled_domains.append(_all_domains[i])
func _toggle_individual_domain(id: int) -> void:
var domain_index = _get_domain_index_from_id(id)
if domain_index < 0 or domain_index >= _all_domains.size():
Loggie.msg("Invalid domain index %d for ID %d" % [domain_index, id]).domain(LoggieConsoleConstants.DOMAIN).warn()
return
var domain_name = _all_domains[domain_index]
var current_state = _domain_popup.is_item_checked(id)
var new_state = not current_state
# Update checkbox
_domain_popup.set_item_checked(id, new_state)
# Update enabled domains list
if new_state and not _enabled_domains.has(domain_name):
_enabled_domains.append(domain_name)
elif not new_state and _enabled_domains.has(domain_name):
_enabled_domains.erase(domain_name)
func _update_all_checkbox() -> void:
var all_enabled = _enabled_domains.size() == _all_domains.size()
_domain_popup.set_item_checked(ALL_ITEM_ID, all_enabled)
func _update_button_text() -> void:
var enabled_count = _enabled_domains.size()
var total_count = _all_domains.size()
if total_count == 0:
text = "Domains: None"
elif enabled_count == total_count:
text = "Domains: All"
elif enabled_count == 0:
text = "Domains: None"
elif enabled_count == 1:
var domain_name = _enabled_domains[0]
var display_name = _get_domain_display_name(domain_name)
text = "Domains: " + display_name
else:
text = "Domains: %d selected" % enabled_count
func _get_domain_item_id(domain_index: int) -> int:
return DOMAIN_ID_OFFSET + domain_index
func _get_domain_index_from_id(item_id: int) -> int:
return item_id - DOMAIN_ID_OFFSET
## Saves current domain selection state to persistent settings
## Param settings: The settings resource to populate with current domain state
func save_settings_to_resource(settings: LoggieConsoleSettings) -> void:
settings.enabled_domains = _enabled_domains.duplicate()
settings.all_known_domains = _all_domains.duplicate()
## Loads domain selection state from persistent settings and rebuilds UI
## Automatically validates that enabled domains still exist in the known domains list
## Param settings: The settings resource containing saved domain state
func load_settings_from_resource(settings: LoggieConsoleSettings) -> void:
_all_domains = settings.all_known_domains.duplicate()
_enabled_domains = settings.enabled_domains.duplicate()
# Remove any enabled domains that no longer exist in all_domains
var valid_enabled_domains: Array[String] = []
for domain in _enabled_domains:
if _all_domains.has(domain):
valid_enabled_domains.append(domain)
_enabled_domains = valid_enabled_domains
# Rebuild UI to reflect loaded state
if _all_domains.size() > 0:
_rebuild_popup()
_update_button_text()
else:
_initialize_empty_state()
@@ -0,0 +1 @@
uid://bq6xlpjg4qoca
@@ -0,0 +1,209 @@
class_name LogBuffer extends RichTextLabel
## Enhanced message buffer with retroactive filtering capabilities
## Stores all messages and applies filters dynamically without losing data
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
const DomainColorManager = preload("res://addons/loggie-console/scripts/domain_color_manager.gd")
# Signals
signal filter_changed(visible_count: int, total_count: int)
# Internal message storage
var _all_messages: Array[LogMessage] = []
var _filtered_messages: Array[LogMessage] = []
var _current_filters: FilterState
# Domain color manager reference
var _color_manager: DomainColorManager
# Memory management constants
const DEFAULT_MAX_MESSAGES: int = 5000
const CLEANUP_PERCENTAGE: float = 0.2 # Remove 20% of messages when limit reached
# Memory management
@export var max_messages: int = DEFAULT_MAX_MESSAGES
## Internal LogMessage class for storing complete message data
class LogMessage:
var msg: LoggieMsg
var preprocessed_content: String
var msg_type: LoggieEnums.MsgType
var log_level: LoggieEnums.LogLevel
var timestamp: float
var enhanced_content: String
func _init(p_msg: LoggieMsg, p_content: String, p_type: LoggieEnums.MsgType, p_level: LoggieEnums.LogLevel) -> void:
msg = p_msg
preprocessed_content = p_content
msg_type = p_type
log_level = p_level
timestamp = Time.get_unix_time_from_system()
## Filter state for retroactive filtering
class FilterState:
var enabled_domains: Array[String] = []
var min_log_level: LoggieEnums.LogLevel = LoggieEnums.LogLevel.DEBUG
var text_search: String = ""
var show_stack_only: bool = false
func _init() -> void:
# Initialize to show ERROR and above by default (lower numbers = higher priority)
min_log_level = LoggieEnums.LogLevel.DEBUG
func _ready() -> void:
_current_filters = FilterState.new()
bbcode_enabled = true
## Set the domain color manager instance
func set_color_manager(color_manager: DomainColorManager) -> void:
_color_manager = color_manager
## Add a new log message to the buffer
func add_message(msg: LoggieMsg, content: String, msg_type: LoggieEnums.MsgType, log_level: LoggieEnums.LogLevel) -> void:
# Create and store the message
var log_msg: LogMessage = LogMessage.new(msg, content, msg_type, log_level)
log_msg.enhanced_content = _enhance_with_metadata(log_msg)
_all_messages.append(log_msg)
# Manage memory if needed
_manage_memory()
# Apply current filters and rebuild display
_reapply_filters()
## Set new filter state and reapply filters
func set_filters(filter_state: FilterState) -> void:
_current_filters = filter_state
_reapply_filters()
## Set text search filter
func set_text_search(search: String) -> void:
if _current_filters.text_search == search:
return
_current_filters.text_search = search
_reapply_filters()
## Clear all messages
func clear_all() -> void:
_all_messages.clear()
_filtered_messages.clear()
text = ""
clear()
# Emit updated counts
filter_changed.emit(0, 0)
## Get current message counts
func get_message_counts() -> Dictionary:
return {
"total": _all_messages.size(),
"filtered": _filtered_messages.size()
}
## Saves current buffer state to persistent settings resource
## Param settings: The settings resource to populate with current state
func save_settings_to_resource(settings: LoggieConsoleSettings) -> void:
settings.filter_state.from_filter_state(_current_filters)
settings.max_messages = max_messages
## Loads buffer state from persistent settings resource
## Note: Does not immediately apply filters to avoid triggering reapply before messages exist
## Param settings: The settings resource containing saved state
func load_settings_from_resource(settings: LoggieConsoleSettings) -> void:
if settings.filter_state:
_current_filters = settings.filter_state.to_filter_state()
max_messages = settings.max_messages
## Applies previously loaded filter state to existing messages
## Call this after load_settings_from_resource() and message loading is complete
func apply_loaded_filters() -> void:
_reapply_filters()
## Private methods
func _reapply_filters() -> void:
_filtered_messages.clear()
for msg in _all_messages:
if msg != null and _message_passes_filters(msg, _current_filters):
_filtered_messages.append(msg)
_rebuild_display()
# Emit filter change signal
filter_changed.emit(_filtered_messages.size(), _all_messages.size())
func _message_passes_filters(msg: LogMessage, filters: FilterState) -> bool:
# Domain check - if no domains are enabled, show no messages
if filters.enabled_domains.size() == 0:
return false
# If domains are enabled, check if this message's domain is in the enabled list
if not filters.enabled_domains.has(msg.msg.domain_name):
return false
# Level check - show messages at or above the minimum level (lower numbers = higher priority)
if msg.log_level > filters.min_log_level:
return false
# Text search
if not filters.text_search.is_empty():
var search_lower: String = filters.text_search.to_lower()
if not msg.preprocessed_content.to_lower().contains(search_lower):
return false
# Stack filter
if filters.show_stack_only and not msg.msg.appends_stack:
return false
return true
func _rebuild_display() -> void:
# Clear current display
text = ""
clear()
# Add all filtered messages
for msg in _filtered_messages:
text += msg.enhanced_content + "\n"
func _enhance_with_metadata(msg: LogMessage) -> String:
var metadata_parts: Array[String] = []
metadata_parts.append("[color=%s]%s [/color]" % [Color.DIM_GRAY.to_html(false), Time.get_time_string_from_unix_time(int(msg.timestamp))])
if msg.msg.domain_name and not msg.msg.domain_name.is_empty():
var domain_color_html: String = _color_manager.get_domain_color_html(msg.msg.domain_name)
metadata_parts.append("[color=%s]%s[/color]" % [domain_color_html, msg.msg.domain_name])
if msg.msg.domain_name and not msg.msg.domain_name.is_empty():
metadata_parts.append("[color=%s][%s] [/color]" % [_get_color_for_log_level(msg.log_level).to_html(false), LoggieEnums.LogLevel.keys()[msg.log_level]])
return "%s %s" % [" ".join(metadata_parts), msg.msg.content[0]]
func _get_color_for_log_level(level: LoggieEnums.LogLevel) -> Color:
match level:
LoggieEnums.LogLevel.ERROR:
return Color.RED
LoggieEnums.LogLevel.WARN:
return Color.ORANGE
LoggieEnums.LogLevel.NOTICE:
return Color.GREEN
LoggieEnums.LogLevel.INFO:
return Color.WHITE
LoggieEnums.LogLevel.DEBUG, _:
return Color.AQUA
## Manages memory by removing oldest messages when buffer limit exceeded
## Uses CLEANUP_PERCENTAGE constant to determine how many messages to remove
func _manage_memory() -> void:
if _all_messages.size() > max_messages:
# Remove oldest messages based on cleanup percentage
var remove_count: int = int(max_messages * CLEANUP_PERCENTAGE)
_all_messages = _all_messages.slice(remove_count)
# Note: This will cause filtered_messages indices to be invalid,
# but _reapply_filters() will be called which rebuilds it
@@ -0,0 +1 @@
uid://wah8efffydnu
@@ -0,0 +1,62 @@
class_name LogLevelFilter extends OptionButton
## Standalone log level filter component
## Manages log level selection and syncs with Loggie settings
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
# Signals
signal level_changed(new_level: LoggieEnums.LogLevel)
func _ready() -> void:
_setup_level_options()
item_selected.connect(_on_level_selected)
tooltip_text = "Filter by log level"
## Initialize with current Loggie settings
func initialize() -> void:
selected = Loggie.settings.log_level
level_changed.emit(Loggie.settings.log_level)
## Get currently selected log level
func get_current_level() -> LoggieEnums.LogLevel:
if selected >= 0 and selected < get_item_count():
return get_item_id(selected) as LoggieEnums.LogLevel
return LoggieEnums.LogLevel.DEBUG
## Set log level programmatically
func set_log_level(level: LoggieEnums.LogLevel) -> void:
for i in range(get_item_count()):
if get_item_id(i) == level:
selected = i
break
## Private methods
func _setup_level_options() -> void:
clear()
# Add all log levels as options
for value in LoggieEnums.LogLevel.values():
var label: String = LoggieEnums.LogLevel.keys()[value]
add_item(label, value)
func _on_level_selected(index: int) -> void:
var new_level = get_item_id(index) as LoggieEnums.LogLevel
# Update Loggie settings
Loggie.settings.log_level = new_level
# Emit change signal
level_changed.emit(new_level)
## Saves current log level selection to persistent settings
## Param settings: The settings resource to populate with current log level
func save_settings_to_resource(settings: LoggieConsoleSettings) -> void:
settings.set_log_level(get_current_level())
## Loads log level selection from persistent settings and applies it
## Updates the UI control to reflect the loaded level without emitting change signals
## Param settings: The settings resource containing saved log level
func load_settings_from_resource(settings: LoggieConsoleSettings) -> void:
set_log_level(settings.get_log_level())
@@ -0,0 +1 @@
uid://d3l2tmf5gym1d
@@ -0,0 +1,381 @@
class_name PanelDomainSelector extends Control
## Panel-based domain selector component for filtering log messages by domain
## Provides enhanced UI with vertical list, hover "Only" buttons, and Apply/Cancel workflow
## Drop-in replacement for MenuButton-based DomainSelector with identical API
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
const LoggieConsoleConstants = preload("res://addons/loggie-console/scripts/loggie_console_constants.gd")
const DomainItemScene = preload("res://addons/loggie-console/scenes/domain_item.tscn")
const DomainColorManager = preload("res://addons/loggie-console/scripts/domain_color_manager.gd")
# Signals - maintains API compatibility with existing DomainSelector
signal domains_changed(enabled_domains: Array[String])
# UI Components - exported for scene tree assignment
@export var trigger_button: Button
@export var domain_panel: PopupPanel
@export var select_all_button: Button
@export var select_none_button: Button
@export var scroll_container: ScrollContainer
@export var domain_list: VBoxContainer
# Domain management
var _all_domains: Array[String] = []
var _enabled_domains: Array[String] = []
# Domain item tracking
var _domain_items: Array[DomainItem] = []
# Panel state tracking
var _panel_is_open: bool = false
# Color management
var _color_manager: DomainColorManager
# Panel sizing constants
const PANEL_WIDTH: int = 300
const PANEL_MAX_HEIGHT: int = 400
const PANEL_MIN_HEIGHT: int = 150
# Panel positioning constants
const PANEL_VERTICAL_GAP: int = 2
const PANEL_HORIZONTAL_MARGIN: int = 10
# Domain item layout constants
const DOMAIN_ITEM_HEIGHT: int = 32
const PANEL_HEADER_HEIGHT: int = 40
const PANEL_SELECTION_CONTROLS_HEIGHT: int = 28
const PANEL_FOOTER_HEIGHT: int = 40
const PANEL_PADDING: int = 20
## Initializes the panel domain selector component
##
## Sets up UI connections, panel sizing, and attempts to load domains from Loggie.
## If no domains are found, initializes an empty state. All UI components must be
## properly assigned via @export variables in the scene for initialization to succeed.
func _ready() -> void:
if not trigger_button:
_initialize_empty_state()
return
domain_panel.hide()
# Connect UI signals
trigger_button.pressed.connect(_on_trigger_button_pressed)
select_all_button.pressed.connect(_on_select_all_button_pressed)
select_none_button.pressed.connect(_on_select_none_button_pressed)
# Connect popup signals to track state
domain_panel.popup_hide.connect(_on_panel_hidden)
domain_panel.about_to_popup.connect(_on_panel_about_to_show)
# Set initial panel size
domain_panel.size = Vector2(PANEL_WIDTH, PANEL_MIN_HEIGHT)
# Try to initialize with real Loggie domains first
_initialize_domains_from_loggie()
# If no domains found, show empty state
if _all_domains.is_empty():
_initialize_empty_state()
## Set the domain color manager instance
func set_color_manager(color_manager: DomainColorManager) -> void:
_color_manager = color_manager
## Initialize the selector with available domains
func set_domains(domains: Array[String]) -> void:
_all_domains = domains.duplicate()
_enabled_domains = domains.duplicate()
_rebuild_domain_list()
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
## Initialize the selector with all domains, but only enable selected ones
## Console domains appear normally but are unselected by default
func set_domains_with_console_unselected(all_domains: Array[String], enabled_domains: Array[String], _console_domains: Array[String]) -> void:
_all_domains = all_domains.duplicate()
_enabled_domains = enabled_domains.duplicate()
_rebuild_domain_list()
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
## Add a new domain to the selector (for dynamic discovery)
func add_domain(domain_name: String) -> void:
if domain_name.is_empty() or _all_domains.has(domain_name):
return
_all_domains.append(domain_name)
_enabled_domains.append(domain_name)
_add_domain_item(domain_name, true)
_update_trigger_button_text()
_update_panel_size()
domains_changed.emit(_enabled_domains)
## Get currently enabled domains
func get_enabled_domains() -> Array[String]:
return _enabled_domains.duplicate()
## Check if a domain is enabled
func is_domain_enabled(domain_name: String) -> bool:
return _enabled_domains.has(domain_name)
## Saves current domain selection state to persistent settings
func save_settings_to_resource(settings: LoggieConsoleSettings) -> void:
settings.enabled_domains = _enabled_domains.duplicate()
settings.all_known_domains = _all_domains.duplicate()
## Loads domain selection state from persistent settings and rebuilds UI
func load_settings_from_resource(settings: LoggieConsoleSettings) -> void:
_all_domains = settings.all_known_domains.duplicate()
_enabled_domains = settings.enabled_domains.duplicate()
# Remove any enabled domains that no longer exist in all_domains
var valid_enabled_domains: Array[String] = []
for domain in _enabled_domains:
if _all_domains.has(domain):
valid_enabled_domains.append(domain)
_enabled_domains = valid_enabled_domains
# Rebuild UI to reflect loaded state
if _all_domains.size() > 0:
_rebuild_domain_list()
_update_trigger_button_text()
else:
_initialize_empty_state()
## Private methods
func _initialize_empty_state() -> void:
if trigger_button:
trigger_button.text = "Domains: None"
trigger_button.tooltip_text = "No domains available"
func _initialize_domains_from_loggie() -> void:
# Query Loggie for registered domains
if not Loggie:
return
var loggie_domains: Array[String] = []
# Get domains from Loggie's registered domains
for domain_name: String in Loggie.domains.keys():
if not domain_name.is_empty():
loggie_domains.append(domain_name)
# If we found domains, initialize with them
if loggie_domains.size() > 0:
set_domains(loggie_domains)
Loggie.msg("Initialized domain selector with %d domains from Loggie" % loggie_domains.size()).domain(LoggieConsoleConstants.DOMAIN).info()
else:
_initialize_empty_state()
## Refresh domains from Loggie (useful for dynamic discovery)
func refresh_domains_from_loggie() -> void:
if not Loggie:
return
var _current_domains: Array[String] = _all_domains.duplicate()
var loggie_domains: Array[String] = []
# Get current domains from Loggie
for domain_name: String in Loggie.domains.keys():
if not domain_name.is_empty():
loggie_domains.append(domain_name)
# Add any new domains we discovered
var new_domains_added: bool = false
for domain_name in loggie_domains:
if not _all_domains.has(domain_name):
add_domain(domain_name)
new_domains_added = true
if new_domains_added:
Loggie.msg("Discovered new domains from Loggie").domain(LoggieConsoleConstants.DOMAIN).info()
func _on_trigger_button_pressed() -> void:
if _all_domains.is_empty():
return
# Only open panel if not already open (button disabled when open)
if not _panel_is_open:
# Position and show panel
domain_panel.popup()
_position_panel()
func _on_select_all_button_pressed() -> void:
# Select all domains instantly
_enabled_domains = _all_domains.duplicate()
_update_domain_items_state()
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
func _on_select_none_button_pressed() -> void:
# Deselect all domains instantly
_enabled_domains.clear()
_update_domain_items_state()
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
## Intelligently positions the domain selection panel relative to the trigger button
##
## Calculates optimal panel placement considering viewport boundaries and trigger button
## position. Prefers positioning below and left-aligned to the trigger button, but will
## adjust to above or right-aligned if necessary to keep the panel fully visible.
## Ensures minimum margins from viewport edges for accessibility.
func _position_panel() -> void:
if not trigger_button:
return
# Get trigger button global position and size
var button_global_pos: Vector2 = trigger_button.global_position
var button_size: Vector2 = trigger_button.size
await get_tree().process_frame
var panel_size: Vector2 = domain_panel.size
# Get viewport rect for boundary checking
var viewport: Viewport = get_viewport()
var viewport_rect: Rect2 = viewport.get_visible_rect()
# Position directly below button, aligned to left edge
var panel_pos: Vector2i = Vector2i(
button_global_pos.x,
button_global_pos.y + button_size.y + PANEL_VERTICAL_GAP
)
# Check if panel would go off-screen horizontally
if panel_pos.x + panel_size.x > viewport_rect.size.x:
# Align to right edge of button instead
panel_pos.x = button_global_pos.x + button_size.x - panel_size.x
# Ensure still within bounds
panel_pos.x = max(PANEL_HORIZONTAL_MARGIN, panel_pos.x)
# Check if panel would go off-screen vertically
if panel_pos.y + panel_size.y > viewport_rect.size.y:
# Position above button instead
panel_pos.y = button_global_pos.y - panel_size.y - PANEL_VERTICAL_GAP
# Final boundary check
panel_pos.x = max(PANEL_HORIZONTAL_MARGIN, min(panel_pos.x, viewport_rect.size.x - panel_size.x - PANEL_HORIZONTAL_MARGIN))
panel_pos.y = max(PANEL_HORIZONTAL_MARGIN, min(panel_pos.y, viewport_rect.size.y - panel_size.y - PANEL_HORIZONTAL_MARGIN))
domain_panel.position = get_window().position + panel_pos
func _rebuild_domain_list() -> void:
# Clear existing domain items
_clear_domain_items()
# Create new domain items
for domain_name in _all_domains:
var is_enabled: bool = _enabled_domains.has(domain_name)
_add_domain_item(domain_name, is_enabled)
_update_panel_size()
func _clear_domain_items() -> void:
for item in _domain_items:
if is_instance_valid(item):
item.queue_free()
_domain_items.clear()
# Clear any remaining children
for child in domain_list.get_children():
child.queue_free()
func _add_domain_item(domain_name: String, is_enabled: bool) -> void:
var item: DomainItem = DomainItemScene.instantiate()
# Add to scene first
domain_list.add_child(item)
_domain_items.append(item)
# Set color manager if available
if _color_manager:
item.set_color_manager(_color_manager)
# Setup domain after adding to scene tree (ensures _ready is called first)
item.setup_domain(domain_name, is_enabled)
# Connect signals
item.selection_changed.connect(_on_domain_item_selection_changed)
item.only_button_pressed.connect(_on_domain_item_only_pressed)
## Dynamically adjusts panel size based on the number of domain items
##
## Calculates the optimal panel height by summing all component heights (header,
## selection controls, domain items, footer, and padding). Clamps the result between
## minimum and maximum height constraints to ensure usability and prevent overflow.
func _update_panel_size() -> void:
if not domain_panel or _domain_items.is_empty():
return
# Calculate required height based on content
var required_height: int = PANEL_HEADER_HEIGHT + PANEL_SELECTION_CONTROLS_HEIGHT + (_domain_items.size() * DOMAIN_ITEM_HEIGHT) + PANEL_FOOTER_HEIGHT + PANEL_PADDING
var final_height: int = clamp(required_height, PANEL_MIN_HEIGHT, PANEL_MAX_HEIGHT)
domain_panel.size = Vector2(PANEL_WIDTH, final_height)
func _update_trigger_button_text() -> void:
if not trigger_button:
return
var enabled_count: int = _enabled_domains.size()
var total_count: int = _all_domains.size()
if total_count == 0:
trigger_button.text = "Domains: None"
trigger_button.tooltip_text = "No domains available"
elif enabled_count == total_count:
trigger_button.text = "Domains: All"
trigger_button.tooltip_text = "All domains selected (%d)" % total_count
elif enabled_count == 0:
trigger_button.text = "Domains: None"
trigger_button.tooltip_text = "No domains selected"
elif enabled_count == 1:
var domain_name: String = _enabled_domains[0]
var display_name: String = domain_name if not domain_name.is_empty() else "(default)"
trigger_button.text = "Domains: " + display_name
trigger_button.tooltip_text = "Selected: " + display_name
else:
trigger_button.text = "Domains: %d selected" % enabled_count
trigger_button.tooltip_text = "%d of %d domains selected" % [enabled_count, total_count]
func _update_domain_items_state() -> void:
for item in _domain_items:
if is_instance_valid(item):
var domain_name: String = item.get_domain_name()
var is_enabled: bool = _enabled_domains.has(domain_name)
item.set_selected(is_enabled)
func _on_domain_item_selection_changed(domain_name: String, is_selected: bool) -> void:
if is_selected and not _enabled_domains.has(domain_name):
_enabled_domains.append(domain_name)
elif not is_selected and _enabled_domains.has(domain_name):
_enabled_domains.erase(domain_name)
# Update UI and emit change immediately
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
func _on_domain_item_only_pressed(domain_name: String) -> void:
# Clear all selections and select only this domain instantly
_enabled_domains.clear()
_enabled_domains.append(domain_name)
_update_domain_items_state()
_update_trigger_button_text()
domains_changed.emit(_enabled_domains)
func _on_panel_about_to_show() -> void:
_panel_is_open = true
if trigger_button:
trigger_button.disabled = true
func _on_panel_hidden() -> void:
_panel_is_open = false
if trigger_button:
trigger_button.disabled = false
@@ -0,0 +1 @@
uid://bfmpjs4x3fhte
@@ -0,0 +1,123 @@
class_name RestoreButton extends CanvasLayer
## Floating restore button component for minimized LoggieConsole
##
## Self-contained overlay button that appears when the console is minimized.
## Handles its own positioning, styling, and lifecycle management with proper
## cleanup when the console is restored. Designed to be added to the root viewport
## for maximum visibility across all scenes.
const LoggieConsoleConstants = preload("res://addons/loggie-console/scripts/loggie_console_constants.gd")
# Alignment constants
enum Alignment {
TOP_LEFT = 0,
TOP_RIGHT = 1,
BOTTOM_LEFT = 2,
BOTTOM_RIGHT = 3
}
# Signals
signal restore_requested() ## Emitted when user clicks the restore button
@export var restore_button: Button
## Initializes the restore button component and sets up styling
##
## Configures the canvas layer for maximum rendering priority, sets up the
## container to fill the viewport, and applies professional styling to the
## restore button. All UI components must be properly assigned via @export.
func _ready() -> void:
Loggie.msg("RestoreButton _ready() called").domain(LoggieConsoleConstants.DOMAIN).debug()
# Connect button signal
restore_button.pressed.connect(_on_restore_button_pressed)
Loggie.msg("RestoreButton initialization complete").domain(LoggieConsoleConstants.DOMAIN).debug()
## Shows the restore button by adding it to the provided scene tree root
##
## This method expects to be called from a parent that has access to the scene tree.
## The parent should pass the root viewport so this component can add itself properly.
##
## @param root_viewport: The root viewport where the button should be added
func show_restore_button(root_viewport: Window = null) -> void:
Loggie.msg("RestoreButton show_restore_button() method called").domain(LoggieConsoleConstants.DOMAIN).debug()
if not root_viewport:
Loggie.msg("No root viewport available for restore button").domain(LoggieConsoleConstants.DOMAIN).error()
return
root_viewport.add_child(self)
Loggie.msg("Restore button added to root viewport: %s" % root_viewport.name).domain(LoggieConsoleConstants.DOMAIN).debug()
await get_tree().process_frame
# Ensure visibility
visible = true
Loggie.msg("Restore button visibility set to true").domain(LoggieConsoleConstants.DOMAIN).debug()
# Force positioning update and debug info
await get_tree().process_frame
## Hides and removes the restore button from the scene tree
##
## Safely removes the button from its parent and hides it. Handles cleanup
## and ensures proper resource management when the console is restored.
func hide_restore_button() -> void:
Loggie.msg("Hiding restore button").domain(LoggieConsoleConstants.DOMAIN).debug()
visible = false
# Remove from parent if attached
if get_parent():
get_parent().remove_child(self)
Loggie.msg("Restore button removed from parent").domain(LoggieConsoleConstants.DOMAIN).debug()
## Sets the alignment position of the restore button
##
## Updates the button's position flags to align it to the specified corner of the screen.
## @param alignment: The alignment enum value (TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT)
func set_alignment(alignment: Alignment) -> void:
if not restore_button:
Loggie.msg("RestoreButton not found, cannot set alignment").domain(LoggieConsoleConstants.DOMAIN).warn()
return
match alignment:
Alignment.TOP_LEFT:
restore_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN # 0 = left align
restore_button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN # 0 = top align
Alignment.TOP_RIGHT:
restore_button.size_flags_horizontal = Control.SIZE_SHRINK_END # 8 = right align
restore_button.size_flags_vertical = Control.SIZE_SHRINK_BEGIN # 0 = top align
Alignment.BOTTOM_LEFT:
restore_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN # 0 = left align
restore_button.size_flags_vertical = Control.SIZE_SHRINK_END # 8 = bottom align
Alignment.BOTTOM_RIGHT:
restore_button.size_flags_horizontal = Control.SIZE_SHRINK_END # 8 = right align
restore_button.size_flags_vertical = Control.SIZE_SHRINK_END # 8 = bottom align
Loggie.msg("Restore button alignment set to: %s" % _get_alignment_name(alignment)).domain(LoggieConsoleConstants.DOMAIN).debug()
## Gets a human-readable name for the alignment enum
##
## @param alignment: The alignment enum value
## @return: String name of the alignment
func _get_alignment_name(alignment: Alignment) -> String:
match alignment:
Alignment.TOP_LEFT:
return "Top Left"
Alignment.TOP_RIGHT:
return "Top Right"
Alignment.BOTTOM_LEFT:
return "Bottom Left"
Alignment.BOTTOM_RIGHT:
return "Bottom Right"
_:
return "Unknown"
## Handles restore button click events
##
## Emits the restore_requested signal when the button is pressed, allowing
## the parent console to handle the actual restore logic while keeping
## this component focused solely on UI concerns.
func _on_restore_button_pressed() -> void:
Loggie.msg("Restore button pressed").domain(LoggieConsoleConstants.DOMAIN).debug()
restore_requested.emit()
@@ -0,0 +1 @@
uid://dmyd1gquxy42u
@@ -0,0 +1,125 @@
class_name LoggieConsoleSettingsPanel extends AcceptDialog
## Standalone settings panel component for LoggieConsole
##
## This component provides a clean, reusable interface for adjusting console settings
## such as text size multiplier. It emits signals when settings are changed and provides
## methods to get/set current values.
##
## Signals:
## - settings_applied(text_size_multiplier: float): Emitted when Apply is clicked
## - settings_reset(): Emitted when Reset to Default is clicked
##
## Public Methods:
## - set_text_size_multiplier(value: float): Sets the current text size value
## - get_text_size_multiplier() -> float: Gets the current text size value
## - reset_to_defaults(): Resets all settings to default values
## Emitted when the user clicks Apply with current settings
signal settings_applied(text_size_multiplier: float, restore_button_alignment: RestoreButton.Alignment)
## Emitted when the user clicks Reset to Default
signal settings_reset()
# UI component references - assigned via editor
@export var _text_size_slider: HSlider
@export var _text_size_value_label: Label
@export var _reset_button: Button
@export var _alignment_option: OptionButton ## Optional - may be null in simplified versions
# Constants
const DEFAULT_TEXT_SIZE_MULTIPLIER: float = 1.0
const MIN_TEXT_SIZE_MULTIPLIER: float = 0.5
const MAX_TEXT_SIZE_MULTIPLIER: float = 2.0
const DEFAULT_RESTORE_BUTTON_ALIGNMENT: RestoreButton.Alignment = RestoreButton.Alignment.TOP_RIGHT
## Initializes the settings panel and connects internal signals
func _ready() -> void:
# Connect internal UI signals
_text_size_slider.value_changed.connect(_on_text_size_slider_changed)
_reset_button.pressed.connect(_on_reset_button_pressed)
confirmed.connect(_on_apply_pressed)
# Initialize display
_update_text_size_label(_text_size_slider.value)
## Sets the text size multiplier value
##
## Updates the slider position and display label to reflect the new value.
## The value is automatically clamped to the valid range (0.5 to 2.0).
##
## @param value: The text size multiplier (will be clamped to valid range)
func set_text_size_multiplier(value: float) -> void:
var clamped_value: float = clampf(value, MIN_TEXT_SIZE_MULTIPLIER, MAX_TEXT_SIZE_MULTIPLIER)
_text_size_slider.value = clamped_value
_update_text_size_label(clamped_value)
## Gets the current text size multiplier value
##
## @return: The current text size multiplier value from the slider
func get_text_size_multiplier() -> float:
return _text_size_slider.value
## Sets the restore button alignment
##
## Updates the alignment option button to reflect the new value.
## If the alignment option doesn't exist, the setting is ignored.
## @param alignment: The restore button alignment enum value
func set_restore_button_alignment(alignment: RestoreButton.Alignment) -> void:
if _alignment_option:
_alignment_option.selected = alignment as int
## Gets the current restore button alignment
##
## @return: The current restore button alignment as an enum value, or default if option doesn't exist
func get_restore_button_alignment() -> RestoreButton.Alignment:
if _alignment_option:
return _alignment_option.selected as RestoreButton.Alignment
else:
return DEFAULT_RESTORE_BUTTON_ALIGNMENT
## Resets all settings to their default values
##
## This method resets both text size multiplier and alignment to defaults and emits the settings_reset signal.
func reset_to_defaults() -> void:
set_text_size_multiplier(DEFAULT_TEXT_SIZE_MULTIPLIER)
set_restore_button_alignment(DEFAULT_RESTORE_BUTTON_ALIGNMENT)
settings_reset.emit()
## Shows the settings panel centered on screen
##
## Convenience method that wraps popup_centered() for easier API usage.
func show_settings() -> void:
popup_centered()
## Called when the text size slider value changes
##
## Updates the value display label to show the current multiplier.
## @param value: The new slider value
func _on_text_size_slider_changed(value: float) -> void:
_update_text_size_label(value)
## Called when the Reset to Default button is pressed
##
## Resets settings to defaults and emits the settings_reset signal.
func _on_reset_button_pressed() -> void:
reset_to_defaults()
## Called when the Apply button is pressed
##
## Emits the settings_applied signal with the current settings values.
func _on_apply_pressed() -> void:
settings_applied.emit(get_text_size_multiplier(), get_restore_button_alignment())
## Updates the text size value label to show the current multiplier
##
## @param value: The text size multiplier to display
func _update_text_size_label(value: float) -> void:
_text_size_value_label.text = "%.1fx" % value
## Validates that a text size multiplier value is within acceptable bounds
##
## @param value: The value to validate
## @return: True if the value is valid, false otherwise
func is_valid_text_size_multiplier(value: float) -> bool:
return value >= MIN_TEXT_SIZE_MULTIPLIER and value <= MAX_TEXT_SIZE_MULTIPLIER
@@ -0,0 +1 @@
uid://giuyu6vhhgam
@@ -0,0 +1,55 @@
class_name StatusDisplay extends Label
## Standalone status display component for console information
## Shows message counts, filter status, and real-time feedback
func _ready() -> void:
text = "Ready"
horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
## Update status with message and domain information
func update_status(total_messages: int, filtered_messages: int, enabled_domains: Array[String], total_domains: int) -> void:
var status_text = ""
if total_messages == 0:
status_text = "Ready"
else:
# Message count display
if filtered_messages == total_messages:
status_text = "%d msgs" % total_messages
else:
status_text = "%d/%d msgs" % [filtered_messages, total_messages]
# Domain filter info
if total_domains > 0 and enabled_domains.size() < total_domains:
status_text += " | %d/%d domains" % [enabled_domains.size(), total_domains]
text = status_text
## Update status when only message counts change (simpler version)
func update_message_counts(total_messages: int, filtered_messages: int) -> void:
if total_messages == 0:
text = "Ready"
elif filtered_messages == total_messages:
text = "%d msgs" % total_messages
else:
text = "%d/%d msgs" % [filtered_messages, total_messages]
## Show search status when text filtering is active
## @param search_term: Current search filter text
## @param results_count: Number of messages matching the search
## @param total_count: Total number of messages in buffer
func update_search_status(search_term: String, results_count: int, total_count: int) -> void:
if search_term.is_empty():
update_message_counts(total_count, results_count)
else:
text = "Search: \"%s\" - %d/%d msgs" % [search_term, results_count, total_count]
## Show loading/processing status with operation name
## @param operation: Name of the operation being performed
func show_processing_status(operation: String) -> void:
text = "%s..." % operation
## Reset to ready state
func reset() -> void:
text = "Ready"
@@ -0,0 +1 @@
uid://whrim6lbiako
@@ -0,0 +1,167 @@
class_name DomainColorManager extends RefCounted
## Centralized domain color management for LoggieConsole
## Provides consistent color mapping across all console components
## Domain color mapping information
class DomainColorInfo:
var domain_name: String
var color: Color
var palette_index: int
var cycle_count: int
var is_variation: bool
func _init(p_domain_name: String, p_color: Color, p_palette_index: int, p_cycle_count: int) -> void:
domain_name = p_domain_name
color = p_color
palette_index = p_palette_index
cycle_count = p_cycle_count
is_variation = p_cycle_count > 0
## Color usage statistics and diagnostic information
class ColorUsageInfo:
var total_domains: int
var palette_size: int
var current_cycle: int
var next_color_index: int
var remaining_in_current_cycle: int
var has_color_variations: bool
func _init(p_total_domains: int, p_palette_size: int, p_current_cycle: int, p_next_color_index: int, p_remaining_in_current_cycle: int, p_has_color_variations: bool) -> void:
total_domains = p_total_domains
palette_size = p_palette_size
current_cycle = p_current_cycle
next_color_index = p_next_color_index
remaining_in_current_cycle = p_remaining_in_current_cycle
has_color_variations = p_has_color_variations
# Domain color mapping storage
var _domain_colors: Dictionary = {} # String -> DomainColorInfo
var _color_index: int = 0
# Predefined color palette for domains
const DOMAIN_COLOR_PALETTE: Array[Color] = [
Color("#FF6B6B"), # Red
Color("#4ECDC4"), # Teal
Color("#45B7D1"), # Blue
Color("#96CEB4"), # Green
Color("#FECA57"), # Yellow
Color("#FF9FF3"), # Pink
Color("#54A0FF"), # Light Blue
Color("#5F27CD"), # Purple
Color("#00D2D3"), # Cyan
Color("#FF9F43"), # Orange
Color("#10AC84"), # Mint
Color("#EE5A24"), # Dark Orange
Color("#0984E3"), # Royal Blue
Color("#6C5CE7"), # Violet
Color("#A29BFE"), # Light Purple
Color("#FD79A8"), # Rose
Color("#FDCB6E"), # Light Yellow
Color("#6C5CE7"), # Indigo
Color("#00B894"), # Emerald
Color("#E84393") # Magenta
]
## Get or assign a color for a domain
func get_domain_color(domain: String) -> Color:
if domain.is_empty():
return Color.WHITE
if not _domain_colors.has(domain):
# Assign a new color from the palette with potential variations
var palette_index: int = _color_index % DOMAIN_COLOR_PALETTE.size()
var cycle_count: int = _color_index / DOMAIN_COLOR_PALETTE.size()
var base_color: Color = DOMAIN_COLOR_PALETTE[palette_index]
var color: Color = _get_color_variation(base_color, cycle_count)
var new_color_info: DomainColorInfo = DomainColorInfo.new(domain, color, palette_index, cycle_count)
_domain_colors[domain] = new_color_info
_color_index += 1
var existing_color_info: DomainColorInfo = _domain_colors[domain] as DomainColorInfo
return existing_color_info.color
## Get HTML color string for domain (for BBCode)
func get_domain_color_html(domain: String) -> String:
return get_domain_color(domain).to_html(false)
## Check if domain has assigned color
func has_domain_color(domain: String) -> bool:
return _domain_colors.has(domain)
## Get domain color information for a specific domain
func get_domain_color_info(domain: String) -> DomainColorInfo:
if not _domain_colors.has(domain):
# Trigger color assignment by calling get_domain_color
get_domain_color(domain)
return _domain_colors[domain] as DomainColorInfo
## Get all registered domains with their color information
func get_all_domain_color_infos() -> Array[DomainColorInfo]:
var infos: Array[DomainColorInfo] = []
for domain_color_info: DomainColorInfo in _domain_colors.values():
infos.append(domain_color_info)
return infos
## Get diagnostic information about color usage and cycles
func get_color_usage_info() -> ColorUsageInfo:
var current_cycle: int = _color_index / DOMAIN_COLOR_PALETTE.size()
var remaining_in_cycle: int = DOMAIN_COLOR_PALETTE.size() - (_color_index % DOMAIN_COLOR_PALETTE.size())
return ColorUsageInfo.new(
_domain_colors.size(),
DOMAIN_COLOR_PALETTE.size(),
current_cycle,
_color_index % DOMAIN_COLOR_PALETTE.size(),
remaining_in_cycle,
current_cycle > 0
)
## Clear all color assignments (for testing/reset)
func clear_all_colors() -> void:
_domain_colors.clear()
_color_index = 0
## Get the color palette for reference
func get_color_palette() -> Array[Color]:
return DOMAIN_COLOR_PALETTE
## Create color variations for domains beyond the initial palette
## Uses brightness and saturation adjustments to create distinguishable variations
func _get_color_variation(base_color: Color, cycle_count: int) -> Color:
if cycle_count == 0:
# First cycle - use original colors
return base_color
# Get HSV components directly from Color
var hue: float = base_color.h
var saturation: float = base_color.s
var value: float = base_color.v
match cycle_count:
1:
# Second cycle - Increase brightness
value = minf(value * 1.3, 1.0)
2:
# Third cycle - Decrease saturation (more pastel)
saturation = maxf(saturation * 0.6, 0.3)
3:
# Fourth cycle - Decrease brightness
value = maxf(value * 0.7, 0.4)
4:
# Fifth cycle - High saturation, medium brightness
saturation = minf(saturation * 1.2, 1.0)
value = maxf(minf(value * 0.85, 1.0), 0.5)
_:
# Beyond fifth cycle - Use mathematical variations
var brightness_mod: float = 1.0 + (cycle_count % 3) * 0.25 - 0.25 # -0.25, 0, +0.25
var saturation_mod: float = 1.0 + ((cycle_count + 1) % 3) * 0.2 - 0.2 # -0.2, 0, +0.2
value = maxf(minf(value * brightness_mod, 1.0), 0.3)
saturation = maxf(minf(saturation * saturation_mod, 1.0), 0.2)
return Color.from_hsv(hue, saturation, value)
@@ -0,0 +1 @@
uid://x0ut5e0gpyu1
@@ -0,0 +1,66 @@
class_name LoggieConsoleChannel extends LoggieMsgChannel
## Custom Loggie channel that feeds messages directly to the LoggieConsole
## This channel captures all message metadata (level, type, domain, channels, stack info)
## without requiring string parsing
# Reference to the console window that will display messages
var _console_window: LoggieConsole
func _init() -> void:
ID = "loggie_console"
# Enable all preprocessing steps to get complete information
preprocess_flags = (
LoggieEnums.PreprocessStep.APPEND_TIMESTAMPS |
LoggieEnums.PreprocessStep.APPEND_DOMAIN_NAME |
LoggieEnums.PreprocessStep.APPEND_CLASS_NAME
)
## Sets the console window that will receive messages from this channel
## @param window: The console window instance, must not be null
func set_console_window(window: LoggieConsole) -> void:
if window == null:
Loggie.msg("Cannot set null console window").domain(LoggieConsoleConstants.DOMAIN).error()
return
_console_window = window
## Override send method to pass complete message data to console
## @param msg: The Loggie message object containing all metadata
## @param msg_type: The message type enum from Loggie
func send(msg: LoggieMsg, msg_type: LoggieEnums.MsgType) -> void:
if _console_window == null:
Loggie.msg("No console window set, message dropped").domain(LoggieConsoleConstants.DOMAIN).warn()
return
if msg == null:
Loggie.msg("Received null message").domain(LoggieConsoleConstants.DOMAIN).error()
return
# Get the preprocessed text (already formatted by Loggie)
var preprocessed_text = msg.last_preprocess_result
# Determine log level from the message content patterns or message type
var log_level = _determine_log_level(msg, msg_type, preprocessed_text)
# Send complete message data to console
_console_window.receive_log_message(msg, preprocessed_text, msg_type, log_level)
## Determine the log level based on message type and content
func _determine_log_level(_msg: LoggieMsg, msg_type: LoggieEnums.MsgType, content: String) -> LoggieEnums.LogLevel:
# Use message type to infer log level
match msg_type:
LoggieEnums.MsgType.ERROR:
return LoggieEnums.LogLevel.ERROR
LoggieEnums.MsgType.WARN:
return LoggieEnums.LogLevel.WARN
LoggieEnums.MsgType.DEBUG:
return LoggieEnums.LogLevel.DEBUG
LoggieEnums.MsgType.INFO:
# For standard messages, check content patterns to determine level
if content.contains("[NOTICE]"):
return LoggieEnums.LogLevel.NOTICE
else:
return LoggieEnums.LogLevel.INFO
_:
return LoggieEnums.LogLevel.INFO
@@ -0,0 +1 @@
uid://bcehgenffbmr3
@@ -0,0 +1,9 @@
class_name LoggieConsoleConstants extends RefCounted
## Centralized constants for the LoggieConsole addon
##
## Contains all shared constants used across LoggieConsole components to ensure
## consistency and avoid duplication. Use StringName for efficient string operations.
## Domain for all LoggieConsole logging messages
const DOMAIN: StringName = &"LOGGIE_CONSOLE"
@@ -0,0 +1 @@
uid://7iexr4yxwqfg
@@ -0,0 +1,96 @@
class_name ConsoleSettingsManager extends RefCounted
## Manages persistent storage of LoggieConsole settings
##
## Provides robust save/load functionality with automatic backup creation and
## comprehensive error handling. Settings are saved immediately when requested
## (no debouncing) and loaded with fallback to backup file or defaults.
const LoggieConsoleSettings = preload("res://addons/loggie-console/resources/loggie_console_settings.gd")
# File paths and configuration
const SETTINGS_PATH = "user://loggie_console_settings.tres"
const BACKUP_PATH = "user://loggie_console_settings_backup.tres"
const SETTINGS_RESOURCE_TYPE = "LoggieConsoleSettings"
## Loads console settings with comprehensive error handling and fallback chain
##
## Load order: primary file → backup file → create defaults
## All loaded settings are validated and sanitized before returning.
## Returns: A valid LoggieConsoleSettings instance (never null)
func load_settings() -> LoggieConsoleSettings:
var settings: LoggieConsoleSettings = null
# Attempt to load primary settings file
settings = _try_load_file(SETTINGS_PATH, "main settings")
# Fall back to backup file if primary failed
if settings == null:
settings = _try_load_file(BACKUP_PATH, "backup settings")
# Create default settings if both files failed
if settings == null:
Loggie.msg("Creating default console settings").domain(LoggieConsoleConstants.DOMAIN).info()
settings = LoggieConsoleSettings.new()
# Always validate to ensure data integrity
settings.validate()
Loggie.msg("Console settings loaded successfully").domain(LoggieConsoleConstants.DOMAIN).debug()
return settings
## Saves console settings to disk immediately with backup creation
##
## Creates a backup of the existing file before saving, then validates
## the settings and writes them using Godot's ResourceSaver.
## Param settings: The settings to save (must not be null)
## Returns: true if save succeeded, false if it failed
func save_settings(settings: LoggieConsoleSettings) -> bool:
return _save_with_backup(settings)
## Attempts to load a settings file with error handling
## Param path: File path to load from
## Param description: Human-readable description for logging
## Returns: Loaded settings or null if loading failed
func _try_load_file(path: String, description: String) -> LoggieConsoleSettings:
if not FileAccess.file_exists(path):
Loggie.msg("Settings file not found: %s" % path).domain(LoggieConsoleConstants.DOMAIN).debug()
return null
var resource: Resource = ResourceLoader.load(path, SETTINGS_RESOURCE_TYPE)
if resource == null:
Loggie.msg("Failed to load %s from: %s" % [description, path]).domain(LoggieConsoleConstants.DOMAIN).warn()
return null
if not resource is LoggieConsoleSettings:
Loggie.msg("Invalid resource type in %s: %s" % [description, path]).domain(LoggieConsoleConstants.DOMAIN).error()
return null
Loggie.msg("Loaded %s from: %s" % [description, path]).domain(LoggieConsoleConstants.DOMAIN).debug()
return resource as LoggieConsoleSettings
## Saves settings with automatic backup creation
## Param settings: The settings to save
## Returns: true if save succeeded, false otherwise
func _save_with_backup(settings: LoggieConsoleSettings) -> bool:
if not settings:
Loggie.msg("Cannot save null settings").domain(LoggieConsoleConstants.DOMAIN).error()
return false
# Validate settings before saving
settings.validate()
# Create backup of existing file before overwriting
if FileAccess.file_exists(SETTINGS_PATH):
var dir: DirAccess = DirAccess.open("user://")
if dir:
dir.copy(SETTINGS_PATH, BACKUP_PATH)
# Save the validated settings
var error: Error = ResourceSaver.save(settings, SETTINGS_PATH)
if error != OK:
Loggie.msg("Failed to save console settings: %s" % error_string(error)).domain(LoggieConsoleConstants.DOMAIN).error()
return false
Loggie.msg("Console settings saved successfully").domain(LoggieConsoleConstants.DOMAIN).debug()
return true
@@ -0,0 +1 @@
uid://bntgmwyauc4h6
+141
View File
@@ -0,0 +1,141 @@
[gd_resource type="Theme" format=3 uid="uid://c1lnnysnspc6v"]
[ext_resource type="StyleBox" uid="uid://dolgdjpxlvt8f" path="res://addons/loggie-console/assets/inner_panel_recess.tres" id="1_nubd1"]
[ext_resource type="Texture2D" uid="uid://bhps6csrl6lef" path="res://addons/loggie-console/assets/close.png" id="1_rapoa"]
[ext_resource type="StyleBox" uid="uid://bpkqcbx15ejak" path="res://addons/loggie-console/assets/outer_panel.tres" id="2_rapoa"]
[ext_resource type="StyleBox" uid="uid://cfafk3ybc15uo" path="res://addons/loggie-console/assets/outer_window_panel.tres" id="3_f200k"]
[ext_resource type="StyleBox" uid="uid://b7k8yo7jfhpue" path="res://addons/loggie-console/assets/inner_panel.tres" id="3_p21dw"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_normal"]
content_margin_left = 3.0
content_margin_top = 1.0
content_margin_right = 3.0
content_margin_bottom = 1.0
bg_color = Color(0.25, 0.25, 0.25, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.4, 0.4, 0.4, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_checkbox_normal"]
bg_color = Color(0.15, 0.15, 0.15, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.4, 0.4, 0.4, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
bg_color = Color(0.15, 0.15, 0.15, 0.95)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.3, 0.3, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_richtextlabel"]
content_margin_left = 1.0
content_margin_top = 1.0
content_margin_right = 1.0
content_margin_bottom = 1.0
bg_color = Color(0.1, 0.1, 0.1, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.3, 0.3, 0.3, 1)
[sub_resource type="SystemFont" id="SystemFont_f200k"]
font_names = PackedStringArray("Monospace")
font_weight = 800
generate_mipmaps = true
multichannel_signed_distance_field = true
[resource]
Button/colors/font_color = Color(0.9, 0.9, 0.9, 1)
Button/colors/font_disabled_color = Color(0.5, 0.5, 0.5, 1)
Button/colors/font_hover_color = Color(1, 1, 1, 1)
Button/colors/font_pressed_color = Color(0.8, 0.8, 0.8, 1)
Button/font_sizes/font_size = 11
Button/styles/disabled = SubResource("StyleBoxFlat_button_normal")
Button/styles/hover = ExtResource("2_rapoa")
Button/styles/normal = ExtResource("2_rapoa")
Button/styles/pressed = ExtResource("1_nubd1")
CheckBox/colors/font_color = Color(0.9, 0.9, 0.9, 1)
CheckBox/colors/font_disabled_color = Color(0.5, 0.5, 0.5, 1)
CheckBox/colors/font_hover_color = Color(1, 1, 1, 1)
CheckBox/colors/font_pressed_color = Color(0.8, 0.8, 0.8, 1)
CheckBox/font_sizes/font_size = 11
CheckBox/styles/disabled = SubResource("StyleBoxFlat_checkbox_normal")
CheckBox/styles/hover_pressed = ExtResource("2_rapoa")
CheckBox/styles/normal = ExtResource("2_rapoa")
CheckBox/styles/pressed = ExtResource("2_rapoa")
HBoxContainer/constants/separation = 2
LineEdit/colors/caret_color = Color(1, 1, 1, 1)
LineEdit/colors/clear_button_color = Color(0, 0, 0, 1)
LineEdit/colors/clear_button_color_pressed = Color(0, 0, 0, 1)
LineEdit/colors/font_color = Color(1, 1, 1, 1)
LineEdit/colors/font_outline_color = Color(0, 0, 0, 1)
LineEdit/colors/font_placeholder_color = Color(0.447904, 0.509153, 0.407339, 1)
LineEdit/colors/font_selected_color = Color(0.792157, 0.901961, 0.85098, 1)
LineEdit/colors/font_uneditable_color = Color(0, 0, 0, 1)
LineEdit/colors/selection_color = Color(0.188235, 0.219608, 0.262745, 1)
LineEdit/constants/caret_width = 0
LineEdit/constants/minimum_character_width = 0
LineEdit/constants/outline_size = 0
LineEdit/font_sizes/font_size = 12
LineEdit/fonts/font = null
LineEdit/icons/clear = null
LineEdit/styles/focus = ExtResource("1_nubd1")
LineEdit/styles/normal = ExtResource("1_nubd1")
LineEdit/styles/read_only = ExtResource("3_p21dw")
MarginContainer/constants/margin_bottom = 1
MarginContainer/constants/margin_left = 1
MarginContainer/constants/margin_right = 1
MarginContainer/constants/margin_top = 1
MenuButton/colors/font_color = Color(0.9, 0.9, 0.9, 1)
MenuButton/colors/font_disabled_color = Color(0.5, 0.5, 0.5, 1)
MenuButton/colors/font_hover_color = Color(1, 1, 1, 1)
MenuButton/colors/font_pressed_color = Color(0.8, 0.8, 0.8, 1)
MenuButton/font_sizes/font_size = 11
MenuButton/styles/hover = ExtResource("1_nubd1")
MenuButton/styles/normal = ExtResource("2_rapoa")
MenuButton/styles/pressed = ExtResource("2_rapoa")
OptionButton/colors/font_color = Color(0.9, 0.9, 0.9, 1)
OptionButton/colors/font_disabled_color = Color(0.5, 0.5, 0.5, 1)
OptionButton/colors/font_hover_color = Color(1, 1, 1, 1)
OptionButton/colors/font_pressed_color = Color(0.8, 0.8, 0.8, 1)
OptionButton/font_sizes/font_size = 11
OptionButton/styles/disabled = ExtResource("3_f200k")
OptionButton/styles/hover = ExtResource("1_nubd1")
OptionButton/styles/normal = ExtResource("1_nubd1")
OptionButton/styles/pressed = ExtResource("1_nubd1")
Panel/styles/panel = SubResource("StyleBoxFlat_panel")
PopupMenu/colors/font_color = Color(0.9, 0.9, 0.9, 1)
PopupMenu/colors/font_hover_color = Color(1, 1, 1, 1)
PopupMenu/colors/font_separator_color = Color(0.5, 0.5, 0.5, 1)
PopupMenu/font_sizes/font_size = 11
PopupMenu/styles/hover = ExtResource("1_nubd1")
PopupMenu/styles/normal = ExtResource("1_nubd1")
PopupMenu/styles/panel = ExtResource("2_rapoa")
PopupPanel/styles/panel = ExtResource("2_rapoa")
RichTextLabel/colors/default_color = Color(0.9, 0.9, 0.9, 1)
RichTextLabel/colors/selection_color = Color(0.4, 0.6, 0.8, 0.4)
RichTextLabel/font_sizes/bold_font_size = 10
RichTextLabel/font_sizes/bold_italics_font_size = 10
RichTextLabel/font_sizes/italics_font_size = 10
RichTextLabel/font_sizes/mono_font_size = 9
RichTextLabel/font_sizes/normal_font_size = 10
RichTextLabel/styles/normal = SubResource("StyleBoxFlat_richtextlabel")
VBoxContainer/constants/separation = 2
Window/colors/title_color = Color(0.175119, 0.187776, 0.176406, 1)
Window/constants/close_h_offset = 18
Window/constants/close_v_offset = 20
Window/constants/title_height = 22
Window/font_sizes/title_font_size = 16
Window/fonts/title_font = SubResource("SystemFont_f200k")
Window/icons/close = ExtResource("1_rapoa")
Window/icons/close_pressed = ExtResource("1_rapoa")
Window/styles/embedded_border = ExtResource("3_f200k")
Window/styles/embedded_unfocused_border = ExtResource("3_f200k")
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2fr6et0qni2y"
path="res://.godot/imported/icon.png-57313fc4d67f18c33a83a3388ad36531.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/icon.png"
dest_files=["res://.godot/imported/icon.png-57313fc4d67f18c33a83a3388ad36531.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://duuj0ibyreygv"
path="res://.godot/imported/logo.png-2771abf7e361d7f10d5859133bc43562.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/logo.png"
dest_files=["res://.godot/imported/logo.png-2771abf7e361d7f10d5859133bc43562.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bfnp2a0sbhi2x"
path="res://.godot/imported/checkbox_checked.png-a77d9d083c4ebd2bf01389a1bba7091b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/theme/checkbox/checkbox_checked.png"
dest_files=["res://.godot/imported/checkbox_checked.png-a77d9d083c4ebd2bf01389a1bba7091b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dqf5cye21gyw8"
path="res://.godot/imported/checkbox_checked_disabled.png-0752d58bb0616edc7e127ccd992cc02f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/theme/checkbox/checkbox_checked_disabled.png"
dest_files=["res://.godot/imported/checkbox_checked_disabled.png-0752d58bb0616edc7e127ccd992cc02f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bib1lwikra5kr"
path="res://.godot/imported/checkbox_unchecked.png-4c82347fbe6c6b60f40739ece2ad0dba.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/theme/checkbox/checkbox_unchecked.png"
dest_files=["res://.godot/imported/checkbox_unchecked.png-4c82347fbe6c6b60f40739ece2ad0dba.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cloe7vx2ej0nf"
path="res://.godot/imported/checkbox_unchecked_disabled.png-aa159b17a13862a51b7ba58c26a64745.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/theme/checkbox/checkbox_unchecked_disabled.png"
dest_files=["res://.godot/imported/checkbox_unchecked_disabled.png-aa159b17a13862a51b7ba58c26a64745.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
@@ -0,0 +1,45 @@
Font: Patrick Hand SC
Source: https://fonts.google.com/specimen/Patrick+Hand+SC/license
Designed by Patrick Wagesreiter
License
Copyright (c) 2010-2012 Patrick Wagesreiter (mail@patrickwagesreiter.at)
This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://btuvtbhws7p8s"
path="res://.godot/imported/PatrickHandSC-Regular.ttf-a768b75713759378d46297f0740f6da5.fontdata"
[deps]
source_file="res://addons/loggie/assets/theme/fonts/PatrickHandSC-Regular.ttf"
dest_files=["res://.godot/imported/PatrickHandSC-Regular.ttf-a768b75713759378d46297f0740f6da5.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,30 @@
Font: Coffee Soda
Source: https://www.creativefabrica.com/product/coffee-soda/ref/53/
--------- AUTHOR'S NOTE ---------
First of All,
Thank you for download my product.
This product 100% free for personal use & commercial use
File include :
1. Coffee Soda.ttf
2. Coffee Soda.otf
More Info :
khurasantype@gmail.com
For Donation:
paypal.me/khurasantype
My OnlineShop:
https://www.creativefabrica.com/ref/53/
My Portofolio:
https://www.behance.net/khurasan
My web:
www.khurasanstudio.com
www.mbkaos.com
Binary file not shown.
@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://e3rpni7mpu0p"
path="res://.godot/imported/coffee_soda.ttf-1c45a1f5c42cb2ffa335471f7a4924a3.fontdata"
[deps]
source_file="res://addons/loggie/assets/theme/fonts/coffee_soda.ttf"
dest_files=["res://.godot/imported/coffee_soda.ttf-1c45a1f5c42cb2ffa335471f7a4924a3.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
@@ -0,0 +1,14 @@
[gd_resource type="StyleBoxFlat" format=3 uid="uid://ckw36egxdynxc"]
[resource]
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.71, 0.52114, 0.1704, 0.772549)
border_blend = true
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
@@ -0,0 +1,95 @@
[gd_resource type="Theme" load_steps=10 format=3 uid="uid://bntkg3oi4b314"]
[ext_resource type="FontFile" uid="uid://btuvtbhws7p8s" path="res://addons/loggie/assets/theme/fonts/PatrickHandSC-Regular.ttf" id="1_ucfms"]
[ext_resource type="Texture2D" uid="uid://bfnp2a0sbhi2x" path="res://addons/loggie/assets/theme/checkbox/checkbox_checked.png" id="2_tqabx"]
[ext_resource type="Texture2D" uid="uid://dqf5cye21gyw8" path="res://addons/loggie/assets/theme/checkbox/checkbox_checked_disabled.png" id="3_plx1a"]
[ext_resource type="Texture2D" uid="uid://bib1lwikra5kr" path="res://addons/loggie/assets/theme/checkbox/checkbox_unchecked.png" id="4_yp55b"]
[ext_resource type="Texture2D" uid="uid://cloe7vx2ej0nf" path="res://addons/loggie/assets/theme/checkbox/checkbox_unchecked_disabled.png" id="5_0424s"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gb03k"]
bg_color = Color(0.349691, 0.30101, 0.201282, 1)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.361244, 0.323458, 0.213348, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
expand_margin_left = 2.0
expand_margin_top = 1.0
expand_margin_right = 2.0
expand_margin_bottom = 1.0
anti_aliasing_size = 0.537
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73pt3"]
bg_color = Color(0.767332, 0.598222, 0.330864, 1)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.943861, 0.775452, 0.522251, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
expand_margin_left = 2.0
expand_margin_top = 1.0
expand_margin_right = 2.0
expand_margin_bottom = 1.0
anti_aliasing_size = 0.537
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xxk7l"]
bg_color = Color(0.661808, 0.507874, 0.261375, 1)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.83, 0.629555, 0.3071, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
expand_margin_left = 2.0
expand_margin_top = 1.0
expand_margin_right = 2.0
expand_margin_bottom = 1.0
anti_aliasing_size = 0.537
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i1fiu"]
bg_color = Color(0.6, 0.4574, 0.228, 1)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.83, 0.629555, 0.3071, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
expand_margin_left = 2.0
expand_margin_top = 1.0
expand_margin_right = 2.0
expand_margin_bottom = 1.0
anti_aliasing_size = 0.537
[resource]
Button/constants/outline_size = 6
Button/font_sizes/font_size = 30
Button/fonts/font = ExtResource("1_ucfms")
Button/styles/disabled = SubResource("StyleBoxFlat_gb03k")
Button/styles/focus = SubResource("StyleBoxFlat_73pt3")
Button/styles/hover = SubResource("StyleBoxFlat_xxk7l")
Button/styles/normal = SubResource("StyleBoxFlat_i1fiu")
CheckBox/icons/checked = ExtResource("2_tqabx")
CheckBox/icons/checked_disabled = ExtResource("3_plx1a")
CheckBox/icons/radio_checked = ExtResource("2_tqabx")
CheckBox/icons/radio_checked_disabled = ExtResource("3_plx1a")
CheckBox/icons/radio_unchecked = ExtResource("4_yp55b")
CheckBox/icons/radio_unchecked_disabled = ExtResource("5_0424s")
CheckBox/icons/unchecked = ExtResource("4_yp55b")
CheckBox/icons/unchecked_disabled = ExtResource("5_0424s")
Label/colors/font_color = Color(0.980392, 0.843137, 0.619608, 1)
Label/constants/outline_size = 3
Label/font_sizes/font_size = 21
Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgh6hd3u8nlpj"
path="res://.godot/imported/updater_bg.png-c90bec38cc1681b029907f694c893180.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/loggie/assets/updater_bg.png"
dest_files=["res://.godot/imported/updater_bg.png-c90bec38cc1681b029907f694c893180.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
@@ -0,0 +1,28 @@
extends LoggieMsgChannel
func _init() -> void:
self.ID = "<your-custom-id>"
# Example - Customize preprocess flags:
# self.preprocess_flags = LoggieEnums.PreprocessStep.APPEND_TIMESTAMPS | LoggieEnums.PreprocessStep.APPEND_DOMAIN_NAME | LoggieEnums.PreprocessStep.APPEND_CLASS_NAME
func send(msg : LoggieMsg, msg_type : LoggieEnums.MsgType):
# Validate that the message is coming from a valid logger if you are going to be requiring access to a [Loggie] instance.
var loggie = msg.get_logger() # Access a [Loggie] instance directly from the message.
if loggie == null:
push_error("Attempt to send a message that's coming from an invalid logger.")
return
# To access the most recently preprocessed version of the message - use:
# msg.last_preprocess_result
# To access a string version of the message (no preprocessing) - use:
var msg_text = msg.string()
# Optionally
# We can use `LoggieTools.convert_string_to_format_mode` to apply one of the Loggie format modes to it.
# Explore more functions in [LoggieTools] and [LoggieMsg].
# var converted = LoggieTools.convert_string_to_format_mode(msg.last_preprocess_result, loggie.settings.msg_format_mode)
# Do something with the message.
print(msg_text)
+75
View File
@@ -0,0 +1,75 @@
class_name DiscordLoggieMsgChannel extends LoggieMsgChannel
const discord_msg_character_limit = 2000 # The max. amount of characters the content of the message can contain before discord refuses to post it.
var debug_domain = "_d_loggie_discord"
var debug_enabled = false
func _init() -> void:
self.ID = "discord"
self.preprocess_flags = 0 # For this type of channel, this will be applied dynamically by Loggie after it loads LoggieSettings.
func send(msg : LoggieMsg, msg_type : LoggieEnums.MsgType):
# Validate variables.
var loggie = msg.get_logger()
if loggie == null:
push_error("Attempt to send a message that's coming from an invalid logger.")
return
# Wait until loggie is inside tree so that we can use add_child(http) on it without errors.
if !loggie.is_inside_tree():
loggie.tree_entered.connect(func():
send(msg, msg_type)
, CONNECT_ONE_SHOT)
return
var webhook_url = loggie.settings.discord_webhook_url_live if loggie.is_in_production() else loggie.settings.discord_webhook_url_dev
if webhook_url == null or (webhook_url is String and webhook_url.is_empty()):
push_error("Attempt to send a message to the Discord channel with an invalid webhook_url.")
return
var output_text = LoggieTools.convert_string_to_format_mode(msg.last_preprocess_result, LoggieEnums.MsgFormatMode.MARKDOWN)
# Chunk the given string into chunks of maximum supported size by Discord, so we don't end up hitting the character limit
# which would prevent the message from getting posted.
var chunks = LoggieTools.chunk_string(output_text, discord_msg_character_limit)
if chunks.size() > 1:
loggie.debug("Chunking a long (", output_text.length(), "length ) message while sending to Discord into:", chunks.size(), "chunks.")
for chunk : String in chunks:
call_deferred("send_post_request", loggie, chunk, webhook_url)
func send_post_request(logger : Variant, output_text : String, webhook_url : String):
# Enable debug messages if configured.
logger.set_domain_enabled(debug_domain, debug_enabled)
# Create a new HTTPRequest POST request that will be sent to Discord and add it into the scenetree.
var http = HTTPRequest.new()
logger.add_child(http)
# When the request is completed, destroy it.
http.request_completed.connect(func(result, response_code, headers, body):
var debug_msg = logger.msg("HTTP Request Completed:").color(Color.ORANGE).header().domain(debug_domain).channel("terminal")
debug_msg.nl().msg("Result:").color(Color.ORANGE).bold().space().msg(result).nl()
debug_msg.msg("Response Code:").color(Color.ORANGE).bold().space().msg(response_code).nl()
debug_msg.msg("Headers:").color(Color.ORANGE).bold().space().msg(headers).nl()
debug_msg.msg("Body:").color(Color.ORANGE).bold().space().msg(body)
debug_msg.debug()
## Inform the user about a received non-success response code.
if response_code < 200 or response_code > 299:
logger.msg("Discord responded with a non-success code: ").bold().msg(response_code, " - This is an indicator that something about the message you tried to send to Discord does not comply with their request body standards (e.g. content is too long, invalid format, etc.)").channel("terminal").warn()
http.queue_free()
)
# Convert the [LoggieMsg]'s contents into markdown and post that to the target webhook url.
var json = JSON.stringify({"content": output_text})
var header = ["Content-Type: application/json"]
# Construct debug message.
if debug_enabled:
var debug_msg_post = logger.msg("Sending POST Request:").color(Color.CORNFLOWER_BLUE).header().channel("terminal").domain(debug_domain).nl()
debug_msg_post.msg("JSON stringified (length {size}):".format({"size": output_text.length()})).color(Color.LIGHT_SLATE_GRAY).bold().space().msg(json).color(Color.SLATE_GRAY)
debug_msg_post.debug()
# Send the request.
http.request(webhook_url, header, HTTPClient.METHOD_POST, json)
+1
View File
@@ -0,0 +1 @@
uid://byj005gxsloio
+65
View File
@@ -0,0 +1,65 @@
class_name SlackLoggieMsgChannel extends LoggieMsgChannel
var debug_domain = "_d_loggie_slack"
var debug_enabled = false
func _init() -> void:
self.ID = "slack"
self.preprocess_flags = 0 # For this type of channel, this will be applied dynamically by Loggie after it loads LoggieSettings.
func send(msg : LoggieMsg, msg_type : LoggieEnums.MsgType):
# Validate variables.
var loggie = msg.get_logger()
if loggie == null:
push_error("Attempt to send a message that's coming from an invalid logger.")
return
# Wait until loggie is inside tree so that we can use add_child(http) on it without errors.
if !loggie.is_inside_tree():
loggie.tree_entered.connect(func():
send(msg, msg_type)
, CONNECT_ONE_SHOT)
return
var webhook = loggie.settings.slack_webhook_url_live if loggie.is_in_production() else loggie.settings.slack_webhook_url_dev
if webhook == null or (webhook is String and webhook.is_empty()):
push_error("Attempt to send a message to the Slack channel with an invalid webhook.")
return
# Enable debug messages if configured.
loggie.set_domain_enabled(debug_domain, debug_enabled)
# Create a new HTTPRequest POST request that will be sent to Slack and add it into the scenetree.
var http = HTTPRequest.new()
loggie.add_child(http)
# When the request is completed, destroy it.
http.request_completed.connect(func(result, response_code, headers, body):
var debug_msg = loggie.msg("HTTP Request Completed:").color(Color.ORANGE).header().domain(debug_domain)
debug_msg.nl().msg("Result:").color(Color.ORANGE).bold().space().msg(result).nl()
debug_msg.msg("Response Code:").color(Color.ORANGE).bold().space().msg(response_code).nl()
debug_msg.msg("Headers:").color(Color.ORANGE).bold().space().msg(headers).nl()
debug_msg.msg("Body:").color(Color.ORANGE).bold().space().msg(body)
debug_msg.debug()
## Inform the user about a received non-success response code.
if response_code < 200 or response_code > 299:
loggie.msg("Slack responded with a non-success code: ").bold().msg(response_code, " - This is an indicator that something about the message you tried to send to Slack does not comply with their request body standards (e.g. content is too long, invalid format, etc.)").channel("terminal").warn()
http.queue_free()
)
# Convert the [LoggieMsg]'s contents into markdown and post that to the target webhook url.
var md_text = LoggieTools.convert_string_to_format_mode(msg.last_preprocess_result, LoggieEnums.MsgFormatMode.PLAIN)
var json = JSON.stringify({"text": md_text})
var header = ["Content-Type: application/json"]
# Construct debug message.
if debug_enabled:
var debug_msg_post = loggie.msg("Sending POST Request:").color(Color.ORANGE).header().domain(debug_domain).nl()
debug_msg_post.msg("Preprocessed message:").color(Color.ORANGE).bold().space().msg(msg.last_preprocess_result).nl()
debug_msg_post.msg("JSON stringified:").color(Color.ORANGE).bold().space().msg(json)
debug_msg_post.debug()
# Send the request.
http.request(webhook, header, HTTPClient.METHOD_POST, json)
+1
View File
@@ -0,0 +1 @@
uid://dj1jdugf244ho
+21
View File
@@ -0,0 +1,21 @@
class_name TerminalLoggieMsgChannel extends LoggieMsgChannel
func _init() -> void:
self.ID = "terminal"
self.preprocess_flags = 0 # For this type of channel, this will be applied dynamically by Loggie after it loads LoggieSettings.
func send(msg : LoggieMsg, msg_type : LoggieEnums.MsgType):
var loggie = msg.get_logger()
var text = LoggieTools.convert_string_to_format_mode(msg.last_preprocess_result, loggie.settings.msg_format_mode)
match loggie.settings.msg_format_mode:
LoggieEnums.MsgFormatMode.ANSI, LoggieEnums.MsgFormatMode.BBCODE:
print_rich(text)
LoggieEnums.MsgFormatMode.PLAIN, _:
print(text)
# Dump a non-preprocessed terminal-ready version of the message in additional ways if that has been configured.
if msg_type == LoggieEnums.MsgType.ERROR and loggie.settings.print_errors_to_console:
push_error(LoggieTools.convert_string_to_format_mode(msg.string(), LoggieEnums.MsgFormatMode.PLAIN))
if msg_type == LoggieEnums.MsgType.WARN and loggie.settings.print_warnings_to_console:
push_warning(LoggieTools.convert_string_to_format_mode(msg.string(), LoggieEnums.MsgFormatMode.PLAIN))
+1
View File
@@ -0,0 +1 @@
uid://cd82p0vffosco

Some files were not shown because too many files have changed in this diff Show More