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": [
|
"main_sash_position": [
|
||||||
1,
|
1,
|
||||||
793
|
793
|
||||||
|
|||||||
@ -203,33 +203,12 @@ def main():
|
|||||||
app._start_refresh()
|
app._start_refresh()
|
||||||
logger.info("GUI refresh active - widgets will update automatically.")
|
logger.info("GUI refresh active - widgets will update automatically.")
|
||||||
|
|
||||||
# Graceful shutdown handler
|
# Store cleanup references in app for use in _on_close
|
||||||
def shutdown():
|
app._cleanup_time_thread = lambda: setattr(globals(), '_time_update_running', False)
|
||||||
"""Cleanup on application exit."""
|
app._time_update_thread = _time_update_thread
|
||||||
logger.info("Shutting down ARTOS...")
|
|
||||||
|
|
||||||
# Stop time update thread
|
# Note: MainDockingWindow handles WM_DELETE_WINDOW with its own _on_close()
|
||||||
global _time_update_running
|
# which calls os._exit(0) for immediate termination
|
||||||
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()
|
|
||||||
|
|
||||||
app.root.protocol("WM_DELETE_WINDOW", shutdown)
|
|
||||||
|
|
||||||
# Start the GUI main loop
|
# Start the GUI main loop
|
||||||
logger.info("ARTOS GUI ready. Starting 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 .dock_frame import DockFrame
|
||||||
from .workspace_manager import WorkspaceManager
|
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
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext
|
from tkinter import ttk, scrolledtext
|
||||||
import logging
|
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.
|
Logger dockable panel with scrollable text display.
|
||||||
Shows real-time logs from the ARTOS system.
|
Shows real-time logs from the ARTOS system.
|
||||||
@ -24,30 +24,82 @@ class LoggerDock(DockFrame):
|
|||||||
self.existing_widget = existing_widget
|
self.existing_widget = existing_widget
|
||||||
self.existing_handler = existing_handler
|
self.existing_handler = existing_handler
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(parent, title="System Logger", closable=False, **kwargs)
|
||||||
parent,
|
|
||||||
title="System Logger",
|
|
||||||
closable=False, # Logger always visible
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
self.log_handler = existing_handler
|
self.log_handler = existing_handler
|
||||||
|
|
||||||
def populate_content(self):
|
def populate_content(self):
|
||||||
"""Create or reuse the scrollable log text widget."""
|
"""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:
|
if self.existing_widget:
|
||||||
# Reuse existing widget - reparent it properly
|
# Try to reparent the existing widget. If that doesn't render
|
||||||
self.log_text = self.existing_widget
|
# 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
|
||||||
|
|
||||||
# Change the parent of the widget
|
# Read existing content (if any) and current view
|
||||||
self.log_text.master = self.content_frame
|
try:
|
||||||
self.log_text.pack_forget() # Remove from old parent
|
old_text = old_widget.get('1.0', tk.END)
|
||||||
|
except Exception:
|
||||||
|
old_text = ''
|
||||||
|
|
||||||
# Re-pack in new parent
|
# Remove old widget from geometry managers
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
try:
|
||||||
|
old_widget.pack_forget()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Handler already exists and is attached
|
# Create a fresh ScrolledText in our content frame
|
||||||
# Just log that we've transferred
|
self.log_text = scrolledtext.ScrolledText(
|
||||||
import logging
|
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()
|
||||||
|
|
||||||
|
# Log that we've transferred
|
||||||
logger = logging.getLogger('ARTOS')
|
logger = logging.getLogger('ARTOS')
|
||||||
logger.info("System Logger dock created - continuing to capture logs")
|
logger.info("System Logger dock created - continuing to capture logs")
|
||||||
else:
|
else:
|
||||||
@ -61,7 +113,8 @@ class LoggerDock(DockFrame):
|
|||||||
fg='#000000',
|
fg='#000000',
|
||||||
state='disabled' # Read-only
|
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
|
# Configure tags for different log levels
|
||||||
self.log_text.tag_config('INFO', foreground='#0066cc')
|
self.log_text.tag_config('INFO', foreground='#0066cc')
|
||||||
|
|||||||
@ -5,12 +5,12 @@ Contains all MCS controls, commands, and telemetry widgets.
|
|||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
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.control_page import ControlPage
|
||||||
from pymsc.gui.pages.mission_page import MissionPage
|
from pymsc.gui.pages.mission_page import MissionPage
|
||||||
|
|
||||||
|
|
||||||
class MCSDock(DockFrame):
|
class MCSDock(TitlePanel):
|
||||||
"""
|
"""
|
||||||
MCS (Mission Control System) dockable panel.
|
MCS (Mission Control System) dockable panel.
|
||||||
Contains tabs for different MCS functions:
|
Contains tabs for different MCS functions:
|
||||||
@ -27,12 +27,7 @@ class MCSDock(DockFrame):
|
|||||||
"""
|
"""
|
||||||
self.bus_module = bus_module
|
self.bus_module = bus_module
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(parent, title="MCS - Mission Control System", closable=False, **kwargs)
|
||||||
parent,
|
|
||||||
title="MCS - Mission Control System",
|
|
||||||
closable=False, # MCS is always visible
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def populate_content(self):
|
def populate_content(self):
|
||||||
"""Create the MCS tab structure and pages."""
|
"""Create the MCS tab structure and pages."""
|
||||||
|
|||||||
@ -4,10 +4,10 @@ Placeholder Dock: Generic empty dock for future modules.
|
|||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
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.
|
Placeholder dock for modules not yet implemented.
|
||||||
Shows module name and description.
|
Shows module name and description.
|
||||||
@ -23,7 +23,6 @@ class PlaceholderDock(DockFrame):
|
|||||||
**kwargs: Additional DockFrame options
|
**kwargs: Additional DockFrame options
|
||||||
"""
|
"""
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
super().__init__(parent, title=title, **kwargs)
|
super().__init__(parent, title=title, **kwargs)
|
||||||
|
|
||||||
def populate_content(self):
|
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
|
import os
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from .dock_frame import DockFrame
|
from .dock_frame import DockFrame
|
||||||
|
from .title_panel import TitlePanel
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceManager:
|
class WorkspaceManager:
|
||||||
@ -30,7 +31,8 @@ class WorkspaceManager:
|
|||||||
"""
|
"""
|
||||||
self.root = root
|
self.root = root
|
||||||
self.layouts_dir = layouts_dir
|
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
|
# Create layouts directory if needed
|
||||||
if not os.path.exists(self.layouts_dir):
|
if not os.path.exists(self.layouts_dir):
|
||||||
@ -90,8 +92,8 @@ class WorkspaceManager:
|
|||||||
self.bottom_container = ttk.Frame(self.main_pane)
|
self.bottom_container = ttk.Frame(self.main_pane)
|
||||||
self.main_pane.add(self.bottom_container, minsize=150, height=200)
|
self.main_pane.add(self.bottom_container, minsize=150, height=200)
|
||||||
|
|
||||||
def add_dock(self, dock_id: str, dock: DockFrame,
|
def add_dock(self, dock_id: str, dock,
|
||||||
position: str = 'left') -> DockFrame:
|
position: str = 'left'):
|
||||||
"""
|
"""
|
||||||
Register and place a dock in the workspace.
|
Register and place a dock in the workspace.
|
||||||
|
|
||||||
@ -103,6 +105,7 @@ class WorkspaceManager:
|
|||||||
Returns:
|
Returns:
|
||||||
The dock instance
|
The dock instance
|
||||||
"""
|
"""
|
||||||
|
# Accept either DockFrame or TitlePanel (or any widget with packable API)
|
||||||
self.docks[dock_id] = dock
|
self.docks[dock_id] = dock
|
||||||
|
|
||||||
if position == 'left':
|
if position == 'left':
|
||||||
|
|||||||
@ -6,6 +6,9 @@ Modular GUI with dockable panels for different system modules.
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
|
||||||
from pymsc.core.bus_1553_module import Bus1553Module
|
from pymsc.core.bus_1553_module import Bus1553Module
|
||||||
from pymsc.gui.docking.workspace_manager import WorkspaceManager
|
from pymsc.gui.docking.workspace_manager import WorkspaceManager
|
||||||
@ -35,6 +38,8 @@ class MainDockingWindow:
|
|||||||
bus_module: Bus1553Module instance for message access
|
bus_module: Bus1553Module instance for message access
|
||||||
"""
|
"""
|
||||||
self.bus_module = bus_module
|
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
|
# Create main window
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
@ -179,28 +184,39 @@ class MainDockingWindow:
|
|||||||
|
|
||||||
def _start_refresh(self):
|
def _start_refresh(self):
|
||||||
"""Start the GUI refresh loop to update all widgets."""
|
"""Start the GUI refresh loop to update all widgets."""
|
||||||
|
self._refresh_active = True
|
||||||
self.gui_refresh_loop()
|
self.gui_refresh_loop()
|
||||||
|
|
||||||
def _stop_refresh(self):
|
def _stop_refresh(self):
|
||||||
"""Stop refresh loops (cleanup on exit)."""
|
"""Stop refresh loops (cleanup on exit)."""
|
||||||
# The refresh loop will stop automatically when window closes
|
self._refresh_active = False
|
||||||
pass
|
|
||||||
|
|
||||||
def gui_refresh_loop(self):
|
def gui_refresh_loop(self):
|
||||||
"""
|
"""
|
||||||
Periodically refreshes all custom widgets in all visible docks.
|
Periodically refreshes all custom widgets in all visible docks.
|
||||||
This updates telemetry displays from received 1553 messages.
|
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)
|
# First, sync all messages from the 1553 bus (read latest telemetry)
|
||||||
|
try:
|
||||||
if self.bus_module and self.bus_module.is_running:
|
if self.bus_module and self.bus_module.is_running:
|
||||||
self.bus_module.sync_all_messages()
|
self.bus_module.sync_all_messages()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Then refresh widgets in all visible docks
|
# Then refresh widgets in all visible docks
|
||||||
|
try:
|
||||||
for dock_id, dock in self.workspace.docks.items():
|
for dock_id, dock in self.workspace.docks.items():
|
||||||
if dock.is_visible and not dock.is_minimized:
|
if dock.is_visible and not dock.is_minimized:
|
||||||
self._refresh_widgets_recursive(dock.content_frame)
|
self._refresh_widgets_recursive(dock.content_frame)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Schedule next refresh (100ms = 10 Hz update rate)
|
# Schedule next refresh only if still active (100ms = 10 Hz update rate)
|
||||||
|
if self._refresh_active:
|
||||||
self.root.after(100, self.gui_refresh_loop)
|
self.root.after(100, self.gui_refresh_loop)
|
||||||
|
|
||||||
def _refresh_widgets_recursive(self, container: tk.Widget):
|
def _refresh_widgets_recursive(self, container: tk.Widget):
|
||||||
@ -301,13 +317,40 @@ Built with Tkinter and ttkbootstrap
|
|||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self):
|
||||||
"""Handle window close event."""
|
"""Handle window close event."""
|
||||||
|
# Prevent multiple close attempts
|
||||||
|
if self._closing:
|
||||||
|
return
|
||||||
|
self._closing = True
|
||||||
|
|
||||||
# Save layout automatically before closing (includes window geometry)
|
# Save layout automatically before closing (includes window geometry)
|
||||||
|
try:
|
||||||
self.workspace.save_layout("user_layout")
|
self.workspace.save_layout("user_layout")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"):
|
if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"):
|
||||||
self._stop_refresh()
|
# Stop GUI refresh loop FIRST to prevent any further after() calls
|
||||||
self.root.quit()
|
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()
|
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):
|
def run(self):
|
||||||
"""Start the main event loop."""
|
"""Start the main event loop."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user