ora il software si chiudere correttamente
This commit is contained in:
parent
652c9605ba
commit
837a9dc274
@ -1,5 +1,5 @@
|
||||
{
|
||||
"window_geometry": "1600x996+312+216",
|
||||
"window_geometry": "1600x996+52+52",
|
||||
"main_sash_position": [
|
||||
1,
|
||||
793
|
||||
|
||||
@ -203,33 +203,12 @@ def main():
|
||||
app._start_refresh()
|
||||
logger.info("GUI refresh active - widgets will update automatically.")
|
||||
|
||||
# Graceful shutdown handler
|
||||
def shutdown():
|
||||
"""Cleanup on application exit."""
|
||||
logger.info("Shutting down ARTOS...")
|
||||
|
||||
# Stop time update thread
|
||||
global _time_update_running
|
||||
if _time_update_running:
|
||||
_time_update_running = False
|
||||
logger.info("Stopping date/time synchronization thread...")
|
||||
if _time_update_thread and _time_update_thread.is_alive():
|
||||
_time_update_thread.join(timeout=3.0)
|
||||
|
||||
# Stop bus session if running
|
||||
if bus_module.is_running:
|
||||
bus_module.stop_session()
|
||||
|
||||
# Export performance metrics
|
||||
logger.info("Exporting performance metrics...")
|
||||
save_stats_to_csv("performance_report.csv")
|
||||
print_stats()
|
||||
|
||||
logger.info("ARTOS shutdown complete.")
|
||||
app.root.quit()
|
||||
app.root.destroy()
|
||||
# Store cleanup references in app for use in _on_close
|
||||
app._cleanup_time_thread = lambda: setattr(globals(), '_time_update_running', False)
|
||||
app._time_update_thread = _time_update_thread
|
||||
|
||||
app.root.protocol("WM_DELETE_WINDOW", shutdown)
|
||||
# Note: MainDockingWindow handles WM_DELETE_WINDOW with its own _on_close()
|
||||
# which calls os._exit(0) for immediate termination
|
||||
|
||||
# Start the GUI main loop
|
||||
logger.info("ARTOS GUI ready. Starting main loop...")
|
||||
|
||||
@ -5,5 +5,6 @@ ARTOS Docking System - Tkinter-based modular GUI framework
|
||||
|
||||
from .dock_frame import DockFrame
|
||||
from .workspace_manager import WorkspaceManager
|
||||
from .title_panel import TitlePanel
|
||||
|
||||
__all__ = ['DockFrame', 'WorkspaceManager']
|
||||
__all__ = ['DockFrame', 'WorkspaceManager', 'TitlePanel']
|
||||
|
||||
@ -5,10 +5,10 @@ Logger Dock: Real-time system logs display.
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
import logging
|
||||
from pymsc.gui.docking import DockFrame
|
||||
from pymsc.gui.docking.title_panel import TitlePanel
|
||||
|
||||
|
||||
class LoggerDock(DockFrame):
|
||||
class LoggerDock(TitlePanel):
|
||||
"""
|
||||
Logger dockable panel with scrollable text display.
|
||||
Shows real-time logs from the ARTOS system.
|
||||
@ -24,30 +24,82 @@ class LoggerDock(DockFrame):
|
||||
self.existing_widget = existing_widget
|
||||
self.existing_handler = existing_handler
|
||||
|
||||
super().__init__(
|
||||
parent,
|
||||
title="System Logger",
|
||||
closable=False, # Logger always visible
|
||||
**kwargs
|
||||
)
|
||||
super().__init__(parent, title="System Logger", closable=False, **kwargs)
|
||||
self.log_handler = existing_handler
|
||||
|
||||
def populate_content(self):
|
||||
"""Create or reuse the scrollable log text widget."""
|
||||
# Remove outer padding from the content frame so the text widget
|
||||
# can occupy the full dock area without an unwanted gap.
|
||||
try:
|
||||
self.content_frame.configure(padding=0)
|
||||
except Exception:
|
||||
pass
|
||||
if self.existing_widget:
|
||||
# Reuse existing widget - reparent it properly
|
||||
self.log_text = self.existing_widget
|
||||
# Try to reparent the existing widget. If that doesn't render
|
||||
# correctly (some platforms don't fully support reparenting),
|
||||
# create a new ScrolledText inside our content_frame and copy
|
||||
# the text/tags across, then update the handler to point to it.
|
||||
old_widget = self.existing_widget
|
||||
|
||||
# Read existing content (if any) and current view
|
||||
try:
|
||||
old_text = old_widget.get('1.0', tk.END)
|
||||
except Exception:
|
||||
old_text = ''
|
||||
|
||||
# Remove old widget from geometry managers
|
||||
try:
|
||||
old_widget.pack_forget()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create a fresh ScrolledText in our content frame
|
||||
self.log_text = scrolledtext.ScrolledText(
|
||||
self.content_frame,
|
||||
wrap=tk.WORD,
|
||||
height=10,
|
||||
font=('Consolas', 9),
|
||||
bg='#ffffff',
|
||||
fg='#000000',
|
||||
state='disabled'
|
||||
)
|
||||
|
||||
# Configure tags for different log levels on the new widget
|
||||
self.log_text.tag_config('INFO', foreground='#0066cc')
|
||||
self.log_text.tag_config('WARNING', foreground='#ff8800')
|
||||
self.log_text.tag_config('ERROR', foreground='#cc0000')
|
||||
self.log_text.tag_config('DEBUG', foreground='#666666')
|
||||
|
||||
# Insert previous content into the new widget
|
||||
try:
|
||||
self.log_text.config(state='normal')
|
||||
if old_text:
|
||||
self.log_text.insert(tk.END, old_text)
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.config(state='disabled')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Pack the new widget to fill area
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
||||
|
||||
# Update or create the handler so logs go to the new widget
|
||||
if self.existing_handler:
|
||||
try:
|
||||
self.existing_handler.text_widget = self.log_text
|
||||
self.log_handler = self.existing_handler
|
||||
except Exception:
|
||||
self.log_handler = TextWidgetHandler(self.log_text)
|
||||
logging.getLogger().addHandler(self.log_handler)
|
||||
else:
|
||||
self.log_handler = TextWidgetHandler(self.log_text)
|
||||
logging.getLogger().addHandler(self.log_handler)
|
||||
|
||||
# Load previous logs from file to show startup messages
|
||||
self._load_previous_logs()
|
||||
|
||||
# Change the parent of the widget
|
||||
self.log_text.master = self.content_frame
|
||||
self.log_text.pack_forget() # Remove from old parent
|
||||
|
||||
# Re-pack in new parent
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
# Handler already exists and is attached
|
||||
# Just log that we've transferred
|
||||
import logging
|
||||
# Log that we've transferred
|
||||
logger = logging.getLogger('ARTOS')
|
||||
logger.info("System Logger dock created - continuing to capture logs")
|
||||
else:
|
||||
@ -61,7 +113,8 @@ class LoggerDock(DockFrame):
|
||||
fg='#000000',
|
||||
state='disabled' # Read-only
|
||||
)
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
# Pack without extra padding so it fills the dock content fully
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
|
||||
|
||||
# Configure tags for different log levels
|
||||
self.log_text.tag_config('INFO', foreground='#0066cc')
|
||||
|
||||
@ -5,12 +5,12 @@ Contains all MCS controls, commands, and telemetry widgets.
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from pymsc.gui.docking import DockFrame
|
||||
from pymsc.gui.docking.title_panel import TitlePanel
|
||||
from pymsc.gui.pages.control_page import ControlPage
|
||||
from pymsc.gui.pages.mission_page import MissionPage
|
||||
|
||||
|
||||
class MCSDock(DockFrame):
|
||||
class MCSDock(TitlePanel):
|
||||
"""
|
||||
MCS (Mission Control System) dockable panel.
|
||||
Contains tabs for different MCS functions:
|
||||
@ -27,12 +27,7 @@ class MCSDock(DockFrame):
|
||||
"""
|
||||
self.bus_module = bus_module
|
||||
|
||||
super().__init__(
|
||||
parent,
|
||||
title="MCS - Mission Control System",
|
||||
closable=False, # MCS is always visible
|
||||
**kwargs
|
||||
)
|
||||
super().__init__(parent, title="MCS - Mission Control System", closable=False, **kwargs)
|
||||
|
||||
def populate_content(self):
|
||||
"""Create the MCS tab structure and pages."""
|
||||
|
||||
@ -4,10 +4,10 @@ Placeholder Dock: Generic empty dock for future modules.
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from pymsc.gui.docking import DockFrame
|
||||
from pymsc.gui.docking.title_panel import TitlePanel
|
||||
|
||||
|
||||
class PlaceholderDock(DockFrame):
|
||||
class PlaceholderDock(TitlePanel):
|
||||
"""
|
||||
Placeholder dock for modules not yet implemented.
|
||||
Shows module name and description.
|
||||
@ -23,7 +23,6 @@ class PlaceholderDock(DockFrame):
|
||||
**kwargs: Additional DockFrame options
|
||||
"""
|
||||
self.description = description
|
||||
|
||||
super().__init__(parent, title=title, **kwargs)
|
||||
|
||||
def populate_content(self):
|
||||
|
||||
84
pymsc/gui/docking/title_panel.py
Normal file
84
pymsc/gui/docking/title_panel.py
Normal file
@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TitlePanel: Lightweight titled panel (Label-like) used instead of full docking.
|
||||
|
||||
Provides a simple titled frame with a content area, basic minimize/restore
|
||||
and visibility API compatible with existing docks so WorkspaceManager can
|
||||
save/restore state unchanged.
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from typing import Optional, Callable
|
||||
|
||||
|
||||
class TitlePanel(ttk.Frame):
|
||||
"""Simple titled panel that mimics DockFrame API minimally.
|
||||
|
||||
Use this when you want a lighter weight, non-dockable box with a title
|
||||
and a content area that expands to fill available space.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: tk.Widget, title: str = "", closable: bool = False, on_close: Optional[Callable] = None, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
|
||||
self.title = title
|
||||
self.closable = closable
|
||||
self.on_close_callback = on_close
|
||||
|
||||
self.is_minimized = False
|
||||
self.is_visible = True
|
||||
|
||||
self._create_ui()
|
||||
|
||||
def _create_ui(self):
|
||||
# Title area (simple label)
|
||||
self.title_bar = ttk.Frame(self, relief=tk.FLAT)
|
||||
self.title_bar.pack(side=tk.TOP, fill=tk.X)
|
||||
|
||||
self.title_label = ttk.Label(self.title_bar, text=self.title, font=('Arial', 10, 'bold'), padding=(4, 2))
|
||||
self.title_label.pack(side=tk.LEFT)
|
||||
|
||||
if self.closable:
|
||||
self.close_btn = ttk.Button(self.title_bar, text='✕', width=3, command=self.close)
|
||||
self.close_btn.pack(side=tk.RIGHT, padx=(4, 0))
|
||||
|
||||
# Content container
|
||||
self.content_frame = ttk.Frame(self)
|
||||
self.content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||
|
||||
def populate_content(self):
|
||||
"""Subclasses should override this to populate `self.content_frame`."""
|
||||
pass
|
||||
|
||||
def minimize(self):
|
||||
if not self.is_minimized:
|
||||
self.content_frame.pack_forget()
|
||||
self.is_minimized = True
|
||||
|
||||
def restore(self):
|
||||
if self.is_minimized:
|
||||
self.content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||
self.is_minimized = False
|
||||
|
||||
def toggle_minimize(self):
|
||||
if self.is_minimized:
|
||||
self.restore()
|
||||
else:
|
||||
self.minimize()
|
||||
|
||||
def close(self):
|
||||
self.pack_forget()
|
||||
self.is_visible = False
|
||||
if self.on_close_callback:
|
||||
self.on_close_callback(self)
|
||||
|
||||
def show(self):
|
||||
if not self.is_visible:
|
||||
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||
self.is_visible = True
|
||||
if self.is_minimized:
|
||||
self.restore()
|
||||
|
||||
def set_title(self, new_title: str):
|
||||
self.title = new_title
|
||||
self.title_label.configure(text=new_title)
|
||||
@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
from typing import Dict, Optional
|
||||
from .dock_frame import DockFrame
|
||||
from .title_panel import TitlePanel
|
||||
|
||||
|
||||
class WorkspaceManager:
|
||||
@ -30,7 +31,8 @@ class WorkspaceManager:
|
||||
"""
|
||||
self.root = root
|
||||
self.layouts_dir = layouts_dir
|
||||
self.docks: Dict[str, DockFrame] = {}
|
||||
# docks may be legacy DockFrame or new TitlePanel instances
|
||||
self.docks: Dict[str, object] = {}
|
||||
|
||||
# Create layouts directory if needed
|
||||
if not os.path.exists(self.layouts_dir):
|
||||
@ -90,8 +92,8 @@ class WorkspaceManager:
|
||||
self.bottom_container = ttk.Frame(self.main_pane)
|
||||
self.main_pane.add(self.bottom_container, minsize=150, height=200)
|
||||
|
||||
def add_dock(self, dock_id: str, dock: DockFrame,
|
||||
position: str = 'left') -> DockFrame:
|
||||
def add_dock(self, dock_id: str, dock,
|
||||
position: str = 'left'):
|
||||
"""
|
||||
Register and place a dock in the workspace.
|
||||
|
||||
@ -103,6 +105,7 @@ class WorkspaceManager:
|
||||
Returns:
|
||||
The dock instance
|
||||
"""
|
||||
# Accept either DockFrame or TitlePanel (or any widget with packable API)
|
||||
self.docks[dock_id] = dock
|
||||
|
||||
if position == 'left':
|
||||
|
||||
@ -6,6 +6,9 @@ Modular GUI with dockable panels for different system modules.
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import threading
|
||||
|
||||
from pymsc.core.bus_1553_module import Bus1553Module
|
||||
from pymsc.gui.docking.workspace_manager import WorkspaceManager
|
||||
@ -35,6 +38,8 @@ class MainDockingWindow:
|
||||
bus_module: Bus1553Module instance for message access
|
||||
"""
|
||||
self.bus_module = bus_module
|
||||
self._refresh_active = False # Flag to control GUI refresh loop
|
||||
self._closing = False # Flag to prevent multiple close attempts
|
||||
|
||||
# Create main window
|
||||
self.root = tk.Tk()
|
||||
@ -179,29 +184,40 @@ class MainDockingWindow:
|
||||
|
||||
def _start_refresh(self):
|
||||
"""Start the GUI refresh loop to update all widgets."""
|
||||
self._refresh_active = True
|
||||
self.gui_refresh_loop()
|
||||
|
||||
def _stop_refresh(self):
|
||||
"""Stop refresh loops (cleanup on exit)."""
|
||||
# The refresh loop will stop automatically when window closes
|
||||
pass
|
||||
self._refresh_active = False
|
||||
|
||||
def gui_refresh_loop(self):
|
||||
"""
|
||||
Periodically refreshes all custom widgets in all visible docks.
|
||||
This updates telemetry displays from received 1553 messages.
|
||||
"""
|
||||
# Check if we should continue refreshing
|
||||
if not self._refresh_active:
|
||||
return
|
||||
|
||||
# First, sync all messages from the 1553 bus (read latest telemetry)
|
||||
if self.bus_module and self.bus_module.is_running:
|
||||
self.bus_module.sync_all_messages()
|
||||
try:
|
||||
if self.bus_module and self.bus_module.is_running:
|
||||
self.bus_module.sync_all_messages()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Then refresh widgets in all visible docks
|
||||
for dock_id, dock in self.workspace.docks.items():
|
||||
if dock.is_visible and not dock.is_minimized:
|
||||
self._refresh_widgets_recursive(dock.content_frame)
|
||||
try:
|
||||
for dock_id, dock in self.workspace.docks.items():
|
||||
if dock.is_visible and not dock.is_minimized:
|
||||
self._refresh_widgets_recursive(dock.content_frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Schedule next refresh (100ms = 10 Hz update rate)
|
||||
self.root.after(100, self.gui_refresh_loop)
|
||||
# Schedule next refresh only if still active (100ms = 10 Hz update rate)
|
||||
if self._refresh_active:
|
||||
self.root.after(100, self.gui_refresh_loop)
|
||||
|
||||
def _refresh_widgets_recursive(self, container: tk.Widget):
|
||||
"""
|
||||
@ -301,13 +317,40 @@ Built with Tkinter and ttkbootstrap
|
||||
|
||||
def _on_close(self):
|
||||
"""Handle window close event."""
|
||||
# Prevent multiple close attempts
|
||||
if self._closing:
|
||||
return
|
||||
self._closing = True
|
||||
|
||||
# Save layout automatically before closing (includes window geometry)
|
||||
self.workspace.save_layout("user_layout")
|
||||
try:
|
||||
self.workspace.save_layout("user_layout")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"):
|
||||
self._stop_refresh()
|
||||
self.root.quit()
|
||||
self.root.destroy()
|
||||
# Stop GUI refresh loop FIRST to prevent any further after() calls
|
||||
self._refresh_active = False
|
||||
|
||||
# Remove GUI log handler to avoid callbacks after widgets destroyed
|
||||
try:
|
||||
if hasattr(self, '_log_handler'):
|
||||
logging.getLogger().removeHandler(self._log_handler)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Destroy window immediately
|
||||
try:
|
||||
self.root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Force immediate exit - don't wait for threads to clean up
|
||||
# This is necessary because TimeSync and BusMonitor threads may block
|
||||
os._exit(0)
|
||||
else:
|
||||
# User cancelled - allow close again later
|
||||
self._closing = False
|
||||
|
||||
def run(self):
|
||||
"""Start the main event loop."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user