Files
car-os/addons/loggie-console/resources/loggie_console_settings.gd
T
2026-07-04 18:22:23 -06:00

295 lines
12 KiB
GDScript

@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