295 lines
12 KiB
GDScript
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
|