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