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
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")