Files
stormirc/src/ui/pm_window.py
2025-11-11 01:02:38 -05:00

201 lines
7.2 KiB
Python

"""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()