@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