"""Private message window for StormIRC.""" import logging from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QTextEdit, QPushButton, QHBoxLayout, QLabel, QPlainTextEdit ) from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QAccessible from src.ui.autocomplete_textedit import AutocompleteTextEdit from src.config.settings import is_valid_text from src.ui.ui_utils import ( format_message_with_timestamp, apply_speech_settings, DEFAULT_PM_WINDOW_WIDTH, DEFAULT_PM_WINDOW_HEIGHT, PM_MESSAGE_ENTRY_MAX_HEIGHT ) logger = logging.getLogger(__name__) class PMWindow(QMainWindow): """Private message window.""" message_send = Signal(str, str) # Signal(nickname, message) window_closed = Signal(str) # Signal(nickname) when window closes def __init__(self, nickname, irc_client, speech_manager, config_manager, server_name, parent=None): super().__init__(parent) self.nickname = nickname self.irc_client = irc_client self.speech_manager = speech_manager self.config_manager = config_manager self.server_name = server_name self.message_buffer = [] # Set window title with format: "nickname - Private Message" self.setWindowTitle(f"{nickname} - Private Message") self.resize(DEFAULT_PM_WINDOW_WIDTH, DEFAULT_PM_WINDOW_HEIGHT) self.setup_ui() def setup_ui(self): """Set up the user interface.""" central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) # Header with nickname header_label = QLabel(f"Private conversation with {self.nickname}") header_label.setStyleSheet("font-weight: bold; padding: 5px;") layout.addWidget(header_label) # Chat display self.chat_display = QTextEdit() self.chat_display.setReadOnly(True) # Enable cursor for screen reader navigation (same as main window) self.chat_display.setTextInteractionFlags( Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard ) self.chat_display.setFocusPolicy(Qt.StrongFocus) self.chat_display.setAccessibleName(f"Private message conversation with {self.nickname}") self.chat_display.setAccessibleDescription( f"Chat history with {self.nickname}. Use arrow keys to navigate. Press Tab to move to message input." ) layout.addWidget(self.chat_display) # Message input area input_layout = QVBoxLayout() input_label = QLabel("Message:") input_layout.addWidget(input_label) # Use autocomplete text edit for nickname completion self.message_entry = AutocompleteTextEdit() self.message_entry.setAccessibleName(f"Message input for {self.nickname}") self.message_entry.setAccessibleDescription( f"Type your message to {self.nickname} here. Press Enter to send, Shift+Enter for new line." ) self.message_entry.setPlaceholderText(f"Type message to {self.nickname}...") self.message_entry.setMaximumHeight(PM_MESSAGE_ENTRY_MAX_HEIGHT) # Autocomplete will be set up by parent when needed # Connect Enter key handling self.message_entry.installEventFilter(self) input_layout.addWidget(self.message_entry) # Send button button_layout = QHBoxLayout() self.send_button = QPushButton("Send") self.send_button.setAccessibleName("Send message button") self.send_button.clicked.connect(self.on_send_message) button_layout.addWidget(self.send_button) button_layout.addStretch() input_layout.addLayout(button_layout) layout.addLayout(input_layout) # Set focus to message entry self.message_entry.setFocus() def eventFilter(self, obj, event): """Event filter to handle Enter key in message entry.""" from PySide6.QtCore import QEvent if obj == self.message_entry and event.type() == QEvent.KeyPress: # Enter without Shift sends message, Shift+Enter inserts newline if event.key() in (Qt.Key_Return, Qt.Key_Enter): if not (event.modifiers() & Qt.ShiftModifier): self.on_send_message() return True # Event handled return super().eventFilter(obj, event) def keyPressEvent(self, event): """Handle key press events - Ctrl stops all speech, Escape closes window.""" if event.key() == Qt.Key_Control: self.speech_manager.stop() elif event.key() == Qt.Key_Escape: self.close() return super().keyPressEvent(event) def speak_message(self, text: str): """Speak message with PM-specific settings.""" accessibility = self.config_manager.get_accessibility_config() # Check if speech is enabled and auto-read is on if not accessibility.speech_enabled or not accessibility.auto_read_messages: return # Validate text has meaningful content if not is_valid_text(text): return # Get PM-specific settings channel_settings = self.config_manager.get_channel_speech_settings(self.server_name, self.nickname) # Apply PM-specific settings using shared utility apply_speech_settings(self.speech_manager, channel_settings, self.nickname) # Speak the text self.speech_manager.speak(text, priority="message", interrupt=False) def on_send_message(self): """Handle send message.""" message = self.message_entry.toPlainText().strip() if not message: return logger.debug(f"Sending PM to {self.nickname}: {message}") # Emit signal to send message self.message_send.emit(self.nickname, message) # Display our message locally self.add_message(self.irc_client.nickname, message) # Clear input self.message_entry.clear() self.message_entry.setFocus() def add_message(self, sender, message): """Add message to chat display.""" formatted_msg = f"<{sender}> {message}" self.message_buffer.append(formatted_msg) # Update display self.chat_display.append(formatted_msg) # Speak the message is_own_message = (sender == self.irc_client.nickname) if is_own_message: # Check if "read my own messages" is enabled accessibility = self.config_manager.get_accessibility_config() if accessibility.speech_read_own_messages: self.speak_message(formatted_msg) else: # Always speak incoming messages self.speak_message(formatted_msg) logger.debug(f"PM window [{self.nickname}]: Added message from {sender}") def closeEvent(self, event): """Handle window close.""" logger.info(f"PM window closing for {self.nickname}") self.window_closed.emit(self.nickname) super().closeEvent(event) def show_and_raise(self): """Show window and bring to front.""" self.show() self.raise_() self.activateWindow() self.message_entry.setFocus()