132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout
|
|
from PySide6.QtGui import QPixmap, QPainter, QColor, QBrush
|
|
from PySide6.QtCore import Qt
|
|
from app.core.theme import theme_manager
|
|
|
|
|
|
class MessageBubbleWidget(QWidget):
|
|
def __init__(
|
|
self,
|
|
text: str,
|
|
timestamp: str,
|
|
is_own: bool,
|
|
sender_name: str | None = None,
|
|
avatar_path: str | None = None,
|
|
):
|
|
super().__init__()
|
|
self.is_own = is_own
|
|
self.sender_name = sender_name
|
|
self.avatar_path = avatar_path
|
|
self._build_ui(text, timestamp)
|
|
self.update_theme()
|
|
# React to theme changes
|
|
try:
|
|
theme_manager.theme_changed.connect(lambda *_: self.update_theme())
|
|
except Exception:
|
|
pass
|
|
|
|
def _build_ui(self, text: str, timestamp: str):
|
|
# Main row controls left/right alignment
|
|
main_layout = QHBoxLayout(self)
|
|
main_layout.setContentsMargins(6, 2, 6, 2)
|
|
main_layout.setSpacing(8)
|
|
|
|
# Avatar placeholder (visible for incoming)
|
|
self.avatar_label = QLabel()
|
|
self._set_avatar(self.avatar_path)
|
|
|
|
# Stack with optional sender name and the bubble
|
|
stack_layout = QVBoxLayout()
|
|
stack_layout.setSpacing(2)
|
|
stack_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Sender name only for incoming messages (if provided)
|
|
self.name_label = QLabel(self.sender_name or "")
|
|
self.name_label.setVisible(bool(self.sender_name) and not self.is_own)
|
|
|
|
# Bubble frame holds text and timestamp
|
|
self.bubble_frame = QWidget()
|
|
self.bubble_frame.setObjectName("Bubble")
|
|
bubble_layout = QVBoxLayout(self.bubble_frame)
|
|
bubble_layout.setContentsMargins(12, 8, 12, 6)
|
|
bubble_layout.setSpacing(3)
|
|
|
|
self.text_label = QLabel(text)
|
|
self.text_label.setWordWrap(True)
|
|
|
|
self.timestamp_label = QLabel(timestamp)
|
|
|
|
bubble_layout.addWidget(self.text_label)
|
|
bubble_layout.addWidget(self.timestamp_label, 0, Qt.AlignRight if self.is_own else Qt.AlignLeft)
|
|
|
|
stack_layout.addWidget(self.name_label)
|
|
stack_layout.addWidget(self.bubble_frame)
|
|
|
|
# Arrange horizontally based on ownership: right for own, left for others
|
|
if self.is_own:
|
|
main_layout.addStretch(1)
|
|
main_layout.addLayout(stack_layout)
|
|
# Reserve place for avatar on the right, hidden for own
|
|
self.avatar_label.setVisible(False)
|
|
main_layout.addWidget(self.avatar_label)
|
|
else:
|
|
main_layout.addWidget(self.avatar_label)
|
|
main_layout.addLayout(stack_layout)
|
|
main_layout.addStretch(1)
|
|
|
|
# Limit bubble width for readability
|
|
self.bubble_frame.setMaximumWidth(420)
|
|
|
|
def _set_avatar(self, image_path: str | None):
|
|
size = 32
|
|
if image_path:
|
|
pixmap = QPixmap(image_path)
|
|
if pixmap.isNull():
|
|
pixmap = QPixmap(size, size)
|
|
else:
|
|
pixmap = QPixmap(size, size)
|
|
|
|
if pixmap.width() != size or pixmap.height() != size:
|
|
pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
|
|
|
|
# Draw a simple circular placeholder if no image
|
|
if not image_path:
|
|
pixmap.fill(Qt.transparent)
|
|
painter = QPainter(pixmap)
|
|
painter.setRenderHint(QPainter.Antialiasing)
|
|
painter.setBrush(QBrush(QColor("#e0e0e0")))
|
|
painter.setPen(Qt.NoPen)
|
|
painter.drawEllipse(0, 0, size, size)
|
|
painter.end()
|
|
|
|
self.avatar_label.setPixmap(pixmap)
|
|
self.avatar_label.setFixedSize(size, size)
|
|
self.avatar_label.setScaledContents(True)
|
|
|
|
def update_theme(self):
|
|
palette = theme_manager.get_current_palette()
|
|
|
|
if self.is_own:
|
|
bg_color = palette["accent"]
|
|
text_color = "#ffffff"
|
|
timestamp_color = "#e6e6e6"
|
|
name_color = palette["text_secondary"]
|
|
ts_align = Qt.AlignRight
|
|
else:
|
|
bg_color = palette["secondary"]
|
|
text_color = palette["text"]
|
|
timestamp_color = palette["text_secondary"]
|
|
name_color = palette["text_secondary"]
|
|
ts_align = Qt.AlignLeft
|
|
|
|
# Style only the bubble frame
|
|
self.bubble_frame.setStyleSheet(
|
|
f"#Bubble {{ background-color: {bg_color}; border-radius: 10px; }}"
|
|
)
|
|
self.text_label.setStyleSheet(f"color: {text_color}; font-size: 10pt;")
|
|
self.timestamp_label.setStyleSheet(f"color: {timestamp_color}; font-size: 8pt;")
|
|
self.name_label.setStyleSheet(f"color: {name_color}; font-size: 8pt; font-weight: 600;")
|
|
|
|
# Keep timestamp alignment in sync with side
|
|
self.timestamp_label.setAlignment(ts_align)
|