Cloud around the text in QTextEdit

No problem :slight_smile:

To show the time, you probably want to send it in the message payload. So your send method becomes (add from time import time at the top). The time.time() method returns the current unix timestamp.

    def invia_messaggio(self):
        self.pubblica(
            {
                "name": self.nome,
                "message": self.input_per_il_messaggio.text(),
                "time": time(),
            }
        )
        self.input_per_il_messaggio.clear()

When you create the messages in the model, you need to pass this through as a 3rd parameter.

                    self.modello.add_message(
                        io, message_str, self.messaggio.get("time")
                    )

…and update add_message to accept that 3rd timestamp parameter.

    def add_message(self, who, text, timestamp):
        if text:
            self.messages.append((who, text, timestamp))
            self.layoutChanged.emit()

So, at this point the time data is in your model. You just need to –

  1. Update the paint method to draw the time in the bubble.
  2. Update the sizeHint method to add a bit of padding to provide space for the time.

First for sizeHint update the data call to unpack the 3rd value. We can discard it (using _) because we don’t need it to calculate the height. We’re just going to add 20 pixels – the time height is always the same.

    def sizeHint(self, option, index):
        _, text, _ = index.model().data(index, Qt.DisplayRole)
        # ... keep the rest the same.
        return textrect.size() + QSize(0, 20)

In the paint we unpack the timestamp from the data returned from the model.

    def paint(self, painter, option, index):
        painter.save()
        user, text, timestamp = index.model().data(index, Qt.DisplayRole)
        # ... add timestamp param, keep the rest the same until...

        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
       
        # draw the timestamp
        font = painter.font()

        font.setPointSize(7)
        painter.setFont(font)
        painter.setPen(Qt.black)
        time_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
        painter.drawText(textrect.bottomLeft() + QPoint(0, 5), time_str)

        doc = QTextDocument(text)

        # ...continues as before.

textrect.bottomLeft() is the bottom left of our text rectangle, into which we draw our message. By taking that as our starting position for our timestamp, we’ll align directly under it. We add 5px vertically for some padding.

This is the same pattern for adding anything to model views – add it to the model, update the paint & sizeHint methods to draw and create space.

The DeprecationWarning is because we’re passing a float value where an int is expected. It’s a new warning in Python 3.8. You can just convert to an int explicitly by wrapping in int(), e.g.

textrect.setHeight(int(doc.size().height()))

it works perfectly, I have set hours and minutes at the bottom right and it’s really nice, I sent you an email :slight_smile:

@martin

You simply answer problems that we all posed ourselves one day, without having answers, even if it is so as not to need it

Bravo. :+1:

Maybe a new chapter for the book. I wonder what this will give for miniature videos or even a pelit on a timeline. :stuck_out_tongue_winking_eye:

@martin How can I add spaces between the text bubbles in your code?

this code does not work, it closes imediately you click on the send or recieve button

import sys

from PyQt5.QtCore import QAbstractListModel, QMargins, QPoint, QRectF, QSize, Qt
from PyQt5.QtGui import QColor, QFont, QPainter, QTextDocument, QTextOption

# from PyQt5.QtGui import
from PyQt5.QtWidgets import (
    QApplication,
    QLineEdit,
    QListView,
    QMainWindow,
    QPushButton,
    QStyledItemDelegate,
    QVBoxLayout,
    QWidget,
)

USER_ME = 0
USER_THEM = 1

BUBBLE_COLORS = {USER_ME: "#90caf9", USER_THEM: "#a5d6a7"}
USER_TRANSLATE = {USER_ME: QPoint(20, 0), USER_THEM: QPoint(0, 0)}

BUBBLE_PADDING = QMargins(15, 5, 35, 5)
TEXT_PADDING = QMargins(25, 15, 45, 15)


class MessageDelegate(QStyledItemDelegate):
    """
    Draws each message.
    """

    _font = None

    def paint(self, painter, option, index):
        painter.save()
        # Retrieve the user,message uple from our model.data method.
        user, text = index.model().data(index, Qt.DisplayRole)

        trans = USER_TRANSLATE[user]
        painter.translate(trans)

        # option.rect contains our item dimensions. We need to pad it a bit
        # to give us space from the edge to draw our shape.
        bubblerect = option.rect.marginsRemoved(BUBBLE_PADDING)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        # draw the bubble, changing color + arrow position depending on who
        # sent the message. the bubble is a rounded rect, with a triangle in
        # the edge.
        painter.setPen(Qt.NoPen)
        color = QColor(BUBBLE_COLORS[user])
        painter.setBrush(color)
        painter.drawRoundedRect(bubblerect, 10, 10)

        # draw the triangle bubble-pointer, starting from the top left/right.
        if user == USER_ME:
            p1 = bubblerect.topRight()
        else:
            p1 = bubblerect.topLeft()
        painter.drawPolygon(p1 + QPoint(-20, 0), p1 + QPoint(20, 0), p1 + QPoint(0, 20))

        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)

        # draw the text
        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        painter.translate(textrect.topLeft())
        doc.drawContents(painter)
        painter.restore()

    def sizeHint(self, option, index):
        _, text = index.model().data(index, Qt.DisplayRole)
        textrect = option.rect.marginsRemoved(TEXT_PADDING)

        toption = QTextOption()
        toption.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)

        doc = QTextDocument(text)
        doc.setTextWidth(textrect.width())
        doc.setDefaultTextOption(toption)
        doc.setDocumentMargin(0)

        textrect.setHeight(doc.size().height())
        textrect = textrect.marginsAdded(TEXT_PADDING)
        return textrect.size()


class MessageModel(QAbstractListModel):
    def __init__(self, *args, **kwargs):
        super(MessageModel, self).__init__(*args, **kwargs)
        self.messages = []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # Here we pass the delegate the user, message tuple.
            return self.messages[index.row()]

    def setData(self, index, role, value):
        self._size[index.row()]

    def rowCount(self, index):
        return len(self.messages)

    def add_message(self, who, text):
        """
        Add an message to our message list, getting the text from the QLineEdit
        """
        if text:  # Don't add empty strings.
            # Access the list via the model.
            self.messages.append((who, text))
            # Trigger refresh.
            self.layoutChanged.emit()


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        # Layout the UI
        l = QVBoxLayout()

        self.message_input = QLineEdit("Enter message here")

        # Buttons for from/to messages.
        self.btn1 = QPushButton("<")
        self.btn2 = QPushButton(">")

        self.messages = QListView()
        self.messages.setResizeMode(QListView.Adjust)
        # Use our delegate to draw items in this view.
        self.messages.setItemDelegate(MessageDelegate())

        self.model = MessageModel()
        self.messages.setModel(self.model)

        self.btn1.pressed.connect(self.message_to)
        self.btn2.pressed.connect(self.message_from)

        l.addWidget(self.messages)
        l.addWidget(self.message_input)
        l.addWidget(self.btn1)
        l.addWidget(self.btn2)

        self.w = QWidget()
        self.w.setLayout(l)
        self.setCentralWidget(self.w)

    def resizeEvent(self, e):
        self.model.layoutChanged.emit()

    def message_to(self):
        self.model.add_message(USER_ME, self.message_input.text())

    def message_from(self):
        self.model.add_message(USER_THEM, self.message_input.text())


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

There has been a change in the PyQt5 API since this was first written. Change

        textrect.setHeight(doc.size().height())

to

        textrect.setHeight(int(doc.size().height()))

i.e. the value passed to setHeight must be an integer (first mentioned at the bottom of this comment here)

If you run your scripts from the command line you can see the errors they produce (rather than just having the window close). For example, the earlier version of the code output this:

Traceback (most recent call last):
  File "C:\Users\Martin\cloudtext.py", line 88, in sizeHint
    textrect.setHeight(doc.size().height())
TypeError: setHeight(self, h: int): argument 1 has unexpected type 'float'

…which is telling you that the h argument to setHeight should be an int.