""" Main GUI window for StreamLens """ import sys import os from typing import Optional, List, TYPE_CHECKING, Dict try: from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableWidget, QTableWidgetItem, QTextEdit, QMenuBar, QMenu, QFileDialog, QMessageBox, QProgressBar, QStatusBar, QLabel, QHeaderView, QPushButton, QGroupBox, QDockWidget, QToolBar, QButtonGroup, QComboBox, QSpinBox ) from PySide6.QtCore import Qt, QThread, Signal, QTimer from PySide6.QtGui import QAction, QIcon, QFont, QKeySequence, QActionGroup from .dock_panels import FlowListDockWidget, FlowDetailDockWidget # Matplotlib integration - lazy loaded matplotlib = None FigureCanvas = None NavigationToolbar = None Figure = None plt = None def _ensure_matplotlib_gui_loaded(): """Lazy load matplotlib for GUI mode""" global matplotlib, FigureCanvas, NavigationToolbar, Figure, plt if matplotlib is not None: return True try: import matplotlib as mpl matplotlib = mpl matplotlib.use('Qt5Agg') # Use Qt backend for matplotlib from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FC from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NT from matplotlib.figure import Figure as Fig import matplotlib.pyplot as pyplot # Turn off interactive mode to prevent floating windows pyplot.ioff() # Ensure no figure windows are created mpl.rcParams['figure.max_open_warning'] = 0 mpl.rcParams['backend'] = 'Qt5Agg' # Prevent any automatic figure display mpl.pyplot.show = lambda *args, **kwargs: None pyplot.show = lambda *args, **kwargs: None # Also prevent figure.show() original_figure_show = Fig.show Fig.show = lambda self, *args, **kwargs: None FigureCanvas = FC NavigationToolbar = NT Figure = Fig plt = pyplot return True except ImportError as e: print(f"Matplotlib GUI integration not available: {e}") return False except ImportError as e: print(f"GUI dependencies not available: {e}") sys.exit(1) if TYPE_CHECKING: from ..analysis.core import EthernetAnalyzer from ..models.flow_stats import FlowStats class PCAPLoadThread(QThread): """Thread for loading PCAP files without blocking UI""" progress_updated = Signal(int) loading_finished = Signal(object) # Emits the analyzer error_occurred = Signal(str) def __init__(self, file_path: str, parent=None): super().__init__(parent) self.file_path = file_path def run(self): try: from ..analysis.core import EthernetAnalyzer from ..utils.pcap_loader import PCAPLoader # Create analyzer analyzer = EthernetAnalyzer() # Load PCAP loader = PCAPLoader(self.file_path) if not loader.validate_file(): self.error_occurred.emit(f"Invalid PCAP file: {self.file_path}") return packets = loader.load_all() analyzer.all_packets = packets # Process packets with progress updates total_packets = len(packets) for i, packet in enumerate(packets, 1): analyzer._process_single_packet(packet, i) # Update progress every 1000 packets if i % 1000 == 0 or i == total_packets: progress = int((i / total_packets) * 100) self.progress_updated.emit(progress) # Calculate statistics analyzer.calculate_statistics() self.loading_finished.emit(analyzer) except Exception as e: self.error_occurred.emit(str(e)) class StreamLensMainWindow(QMainWindow): """Main application window with docking panels""" def __init__(self): super().__init__() self.analyzer: Optional['EthernetAnalyzer'] = None self.current_file = None self.loading_thread = None self._initializing = True # Flag to prevent early event triggers self.setWindowTitle("StreamLens - Ethernet Traffic Analyzer") self.setGeometry(100, 100, 1600, 1000) # Larger default size for docking # Apply modern dark theme self.apply_dark_theme() self.setup_menus() self.setup_toolbar() self.setup_docking_ui() self.setup_status_bar() # Initialization complete self._initializing = False def apply_dark_theme(self): """Apply modern dark theme to the entire application""" dark_stylesheet = """ /* Main Window Dark Theme */ QMainWindow { background-color: #1e1e1e; color: #ffffff; } /* Menu Bar */ QMenuBar { background-color: #2d2d2d; color: #ffffff; border-bottom: 1px solid #404040; padding: 2px; } QMenuBar::item { background-color: transparent; padding: 8px 12px; border-radius: 4px; } QMenuBar::item:selected { background-color: #404040; } QMenuBar::item:pressed { background-color: #505050; } /* Menu Dropdown */ QMenu { background-color: #2d2d2d; color: #ffffff; border: 1px solid #404040; border-radius: 6px; padding: 4px; } QMenu::item { padding: 8px 20px; border-radius: 4px; } QMenu::item:selected { background-color: #404040; } /* Toolbar */ QToolBar { background-color: #2d2d2d; border: none; padding: 4px; spacing: 8px; } QToolButton { background-color: #404040; color: #ffffff; border: 1px solid #505050; border-radius: 6px; padding: 8px 12px; font-weight: 500; } QToolButton:hover { background-color: #505050; border-color: #606060; } QToolButton:pressed { background-color: #353535; } /* Status Bar */ QStatusBar { background-color: #2d2d2d; color: #cccccc; border-top: 1px solid #404040; padding: 4px; } /* Progress Bar */ QProgressBar { background-color: #404040; border: 1px solid #505050; border-radius: 4px; text-align: center; color: #ffffff; } QProgressBar::chunk { background-color: #0078d4; border-radius: 3px; } /* Spin Box */ QSpinBox { background-color: #404040; color: #ffffff; border: 1px solid #505050; border-radius: 4px; padding: 4px 8px; min-width: 50px; } QSpinBox:hover { border-color: #606060; } QSpinBox:focus { border-color: #0078d4; } QSpinBox::up-button, QSpinBox::down-button { background-color: #505050; border: none; width: 16px; } QSpinBox::up-button:hover, QSpinBox::down-button:hover { background-color: #606060; } /* Labels */ QLabel { color: #ffffff; } /* Dock Widgets */ QDockWidget { background-color: #1e1e1e; color: #ffffff; titlebar-close-icon: none; titlebar-normal-icon: none; } QDockWidget::title { background-color: #2d2d2d; color: #ffffff; padding: 8px; border-bottom: 1px solid #404040; font-weight: bold; } /* Splitter */ QSplitter::handle { background-color: #404040; } QSplitter::handle:horizontal { width: 2px; } QSplitter::handle:vertical { height: 2px; } """ self.setStyleSheet(dark_stylesheet) def setup_docking_ui(self): """Set up the UI layout with integrated plots""" # Create docking panels self.flow_list_dock = FlowListDockWidget(self) self.flow_detail_dock = FlowDetailDockWidget(self) # Set flow list as central widget to take full width self.setCentralWidget(self.flow_list_dock) # Add detail dock at bottom self.addDockWidget(Qt.BottomDockWidgetArea, self.flow_detail_dock) # Connect signals self.flow_list_dock.flows_table.itemSelectionChanged.connect(self.on_flow_selection_changed) # Set detail dock height self.resizeDocks([self.flow_detail_dock], [150], Qt.Vertical) def on_flow_selection_changed(self): """Handle flow selection change to update detail panel""" if not hasattr(self, 'flow_list_dock') or not self.analyzer: return selected_rows = self.flow_list_dock.flows_table.selectionModel().selectedRows() if not selected_rows: self.flow_detail_dock.update_flow_details(None, None) return # Get selected flow row = selected_rows[0].row() flow_item = self.flow_list_dock.flows_table.item(row, 0) flow = flow_item.data(Qt.UserRole) if flow: self.flow_detail_dock.update_flow_details(flow, self.analyzer) def setup_menus(self): """Set up application menus""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu("&File") # Open PCAP open_action = QAction("&Open PCAP...", self) open_action.setShortcut(QKeySequence.Open) open_action.setStatusTip("Open a PCAP file for analysis") open_action.triggered.connect(self.open_pcap_file) file_menu.addAction(open_action) # Monitor NIC monitor_action = QAction("&Monitor NIC...", self) monitor_action.setShortcut("Ctrl+M") monitor_action.setStatusTip("Start live network monitoring") monitor_action.triggered.connect(self.monitor_nic) file_menu.addAction(monitor_action) file_menu.addSeparator() # Exit exit_action = QAction("E&xit", self) exit_action.setShortcut(QKeySequence.Quit) exit_action.setStatusTip("Exit StreamLens") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # View menu view_menu = menubar.addMenu("&View") # Refresh refresh_action = QAction("&Refresh", self) refresh_action.setShortcut(QKeySequence.Refresh) refresh_action.setStatusTip("Refresh current data") refresh_action.triggered.connect(self.refresh_data) view_menu.addAction(refresh_action) view_menu.addSeparator() # Show/Hide panels show_flow_details_action = QAction("Show Flow &Details", self) show_flow_details_action.setCheckable(True) show_flow_details_action.setChecked(True) show_flow_details_action.triggered.connect(lambda checked: self.flow_detail_dock.setVisible(checked)) view_menu.addAction(show_flow_details_action) # Help menu help_menu = menubar.addMenu("&Help") about_action = QAction("&About StreamLens...", self) about_action.setStatusTip("About StreamLens") about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def setup_toolbar(self): """Set up application toolbar""" toolbar = QToolBar("Main Toolbar") toolbar.setMovable(False) self.addToolBar(toolbar) # File operations open_action = QAction("Open", self) open_action.setShortcut(QKeySequence.Open) open_action.setStatusTip("Open PCAP file") open_action.triggered.connect(self.open_pcap_file) toolbar.addAction(open_action) monitor_action = QAction("Monitor", self) monitor_action.setStatusTip("Monitor network interface") monitor_action.triggered.connect(self.monitor_nic) toolbar.addAction(monitor_action) toolbar.addSeparator() # Outlier threshold control toolbar.addWidget(QLabel("Outlier Threshold: ")) self.outlier_threshold_spin = QSpinBox() self.outlier_threshold_spin.setRange(1, 10) self.outlier_threshold_spin.setValue(3) self.outlier_threshold_spin.setSuffix("σ") self.outlier_threshold_spin.setStatusTip("Set sigma threshold for outlier detection") self.outlier_threshold_spin.valueChanged.connect(self.on_outlier_threshold_changed) toolbar.addWidget(self.outlier_threshold_spin) def setup_status_bar(self): """Set up status bar""" self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.status_bar.addPermanentWidget(self.progress_bar) self.status_bar.showMessage("Ready") def open_pcap_file(self): """Open PCAP file dialog""" file_path, _ = QFileDialog.getOpenFileName( self, "Open PCAP File", "", "PCAP Files (*.pcap *.pcapng);;All Files (*)" ) if file_path: self.load_pcap_file(file_path) def load_pcap_file(self, file_path: str): """Load PCAP file in background thread""" if self.loading_thread and self.loading_thread.isRunning(): return self.current_file = file_path self.status_bar.showMessage(f"Loading {os.path.basename(file_path)}...") self.progress_bar.setVisible(True) self.progress_bar.setValue(0) # Disable UI during loading self.flow_list_dock.flows_table.setEnabled(False) # Start loading thread self.loading_thread = PCAPLoadThread(file_path) self.loading_thread.progress_updated.connect(self.progress_bar.setValue) self.loading_thread.loading_finished.connect(self.on_pcap_loaded) self.loading_thread.error_occurred.connect(self.on_loading_error) self.loading_thread.start() def on_pcap_loaded(self, analyzer: 'EthernetAnalyzer'): """Handle PCAP loading completion""" self.analyzer = analyzer self.progress_bar.setVisible(False) self.flow_list_dock.flows_table.setEnabled(True) # Update flow list dock with analyzer and file info self.flow_list_dock.set_analyzer(analyzer) self.flow_list_dock.update_file_info(self.current_file) # Populate flows table with embedded plots (synchronous) self.flow_list_dock.populate_flows_table() summary = analyzer.get_summary() self.status_bar.showMessage(f"Loaded {summary['total_packets']:,} packets with embedded plots") def on_loading_error(self, error_message: str): """Handle loading error""" self.progress_bar.setVisible(False) self.flow_list_dock.flows_table.setEnabled(True) QMessageBox.critical(self, "Loading Error", f"Error loading PCAP file:\n{error_message}") self.status_bar.showMessage("Error loading file") def get_flow_packets(self, flow: 'FlowStats') -> List: """Get all packets for a specific flow""" if not self.analyzer or not self.analyzer.all_packets: return [] flow_packets = [] for packet in self.analyzer.all_packets: try: if hasattr(packet, 'haslayer'): from scapy.all import IP if packet.haslayer(IP): ip_layer = packet[IP] if ip_layer.src == flow.src_ip and ip_layer.dst == flow.dst_ip: flow_packets.append(packet) except: continue return flow_packets def refresh_data(self): """Refresh the current data""" if self.current_file: self.load_pcap_file(self.current_file) def closeEvent(self, event): """Handle application close""" if self.loading_thread and self.loading_thread.isRunning(): self.loading_thread.quit() self.loading_thread.wait() event.accept() # Menu and toolbar action handlers def monitor_nic(self): """Start network interface monitoring""" # TODO: Implement NIC monitoring dialog QMessageBox.information( self, "Monitor NIC", "Network interface monitoring will be implemented in a future version." ) def show_about(self): """Show about dialog""" QMessageBox.about( self, "About StreamLens", """

StreamLens v1.0

Advanced Ethernet Traffic Analyzer

Specialized protocol dissection for aviation and industrial networks with sigma-based outlier identification and interactive signal visualization.

Features:

Built with PySide6 and matplotlib

""" ) def on_outlier_threshold_changed(self, value: int): """Handle outlier threshold change""" if self.analyzer: # Update analyzer's outlier threshold self.analyzer.statistics_engine.sigma_threshold = float(value) # Refresh the flow data with new threshold if hasattr(self, 'flow_list_dock'): self.flow_list_dock.populate_flows_table() self.status_bar.showMessage(f"Outlier threshold updated to {value}σ")