Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. Implementing chat type listview with text bubbles
QtWS25 Last Chance

Implementing chat type listview with text bubbles

Scheduled Pinned Locked Moved Solved General and Desktop
custom itemqlistviewmodel-viewchattingtextsize
12 Posts 4 Posters 5.8k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • B Offline
    B Offline
    bepaald
    wrote on 8 Jun 2018, 15:09 last edited by
    #1

    I am trying to write a little program showing the chat history from a messaging app, trying to stay as close to the original look of the (android) app. The actual chat data is in a SQL database.

    At this moment I am creating a QSqlQueryModel, which holds the data, and a QListView which is supposed to show the data. In between I have a QStyledItemDelegate to do the painting in the view in order to get the custom look of the list items.

    An example of the look I am going for:

    0_1528469756400_example.png

    The main things being the bubble frame around the text, and the text having different sizes and possibly containing images (photos, gifs, emoji), video or audio. One of the main things I'm having trouble with, is that when drawing a bubble in the delegate, I can't seem to find out what size the contents are going to be. For normal text I might be able to use QFontMetrics, but with rich text I'm not sure. I tried putting everything in a QTextEdit since it handles rich text, and through the HTML capabilities also images. Then painting that QTextEdit in de delegates paint() method, but the QTextEdit doesn't seem to know its own size either (probably because its image is being painted, but the QTextEdit itself is never actually show()n).

    Am I on the right track with the model/view/delegate? I have also considered QGraphicsView, but I'm not sure if it would help with anything, and I think I really need the lazy loading of the view, since each chat can contain thousands of messages. Any hints on how to implement this? Even just starting getting this to work with messages containing only text would be a great help. Thanks!

    bepaald

    1 Reply Last reply
    0
    • S Offline
      S Offline
      SGaist
      Lifetime Qt Champion
      wrote on 8 Jun 2018, 21:20 last edited by
      #2

      Hi,

      AFAIK, you're on the right track. I just stumbled upon this for HTML rendering. Might of interest.

      Hope it helps

      Interested in AI ? www.idiap.ch
      Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

      1 Reply Last reply
      3
      • B Offline
        B Offline
        bepaald
        wrote on 27 Jun 2018, 08:12 last edited by
        #3

        Thank you very much! After your message I totally ran with it (and that example you provided helped a great deal), and got it mostly working. I have a few small problems still that I need to work out, but I think they would be better off in a new thread as they would be off topic here.

        Thanks again!

        T 1 Reply Last reply 12 Jan 2019, 11:21
        0
        • B bepaald
          27 Jun 2018, 08:12

          Thank you very much! After your message I totally ran with it (and that example you provided helped a great deal), and got it mostly working. I have a few small problems still that I need to work out, but I think they would be better off in a new thread as they would be off topic here.

          Thanks again!

          T Offline
          T Offline
          Taytoo
          wrote on 12 Jan 2019, 11:21 last edited by
          #4

          @bepaald said in Implementing chat type listview with text bubbles:

          Thank you very much! After your message I totally ran with it (and that example you provided helped a great deal), and got it mostly working. I have a few small problems still that I need to work out, but I think they would be better off in a new thread as they would be off topic here.

          Thanks again!

          I'm trying to create bubble chat for my desktop application. Can you please share how you end up implementing it? Would you mind sharing some of the code? Thanks!

          B 1 Reply Last reply 17 Jan 2019, 17:47
          0
          • B Offline
            B Offline
            bepaald
            wrote on 13 Jan 2019, 11:51 last edited by
            #5

            @Taytoo said in Implementing chat type listview with text bubbles:

            @bepaald said in Implementing chat type listview with text bubbles:

            [...]

            I'm trying to create bubble chat for my desktop application. Can you please share how you end up implementing it? Would you mind sharing some of the code? Thanks!

            Hi Taytoo! As I said I totally ran with it and, after 7 months, the project is now huge (though still far from finished). As it is now sharing the code is impossible, it is thousands of lines of code and probably totally unreadable. I will try to come up with some minimal example in the near future and post it back here, but it will probably take a while as I am very busy.

            Just as a starting point, you might want to check out this link https://stackoverflow.com/questions/1956542/how-to-make-item-view-render-rich-html-text-in-qt/1956781#1956781, I think it was how I started as well.

            1 Reply Last reply
            2
            • T Taytoo
              12 Jan 2019, 11:21

              @bepaald said in Implementing chat type listview with text bubbles:

              Thank you very much! After your message I totally ran with it (and that example you provided helped a great deal), and got it mostly working. I have a few small problems still that I need to work out, but I think they would be better off in a new thread as they would be off topic here.

              Thanks again!

              I'm trying to create bubble chat for my desktop application. Can you please share how you end up implementing it? Would you mind sharing some of the code? Thanks!

              B Offline
              B Offline
              bepaald
              wrote on 17 Jan 2019, 17:47 last edited by
              #6

              @Taytoo
              Ok, I got around to creating a little example. It shows just a normal QListView, but painting the data in its model with a custom delegate. This delegate has two main functions: first it needs to report the size in the view an item is going to need to be painted. Second, it needs to paint.

              The main file just sets up some dummy data in a standard QStandardItemModel. Make sure you add any data to the model that your delegate might need to paint the item properly. In this example I only added an "outgoing" or "incoming" datafield to get some different messages, but you may need a ton more dataroles (I did!). Then it creates a pretty standard QListView. The only custom stuff is the ListViewDelegate set on the view using QListView::setItemDelegate().

              //main.cc
              
              #include <QtWidgets>
              
              #include "listviewdelegate.h"
              
              int main(int argc, char *argv[])
              {
                QApplication app(argc, argv);
              
                // create some data and put it in a model
                QStandardItemModel mymodel;
                QStandardItem *item1 = new QStandardItem("This is item one");
                item1->setData("Incoming", Qt::UserRole + 1);
                mymodel.appendRow(item1);
                QStandardItem *item2 = new QStandardItem("This is item two, it is a very long item, but it's not the item's fault, it is me typing all this text.");
                item2->setData("Outgoing", Qt::UserRole + 1);
                mymodel.appendRow(item2);
                QStandardItem *item3 = new QStandardItem("This is the third item");
                item3->setData("Incoming", Qt::UserRole + 1);
                mymodel.appendRow(item3);
              
                // create a view and set our data
                QListView listview;
                listview.setResizeMode(QListView::Adjust);
                listview.setWordWrap(true);
                listview.setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
                listview.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
                listview.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
                listview.setModel(&mymodel);
                listview.setMinimumSize(200,350);
              
                // NOW tell the view to rely on out custom delegate for drawing
                listview.setItemDelegate(new ListViewDelegate());
              
                // show it
                listview.show();
              
                return app.exec();
              }
              

              As said, the delegate just gives size hints and paints. You can paint anything you want in the items, just make sure to also update the sizeHint() for any new elements you're painting. I am using a QTextDocument to store the body text, as it can easily report its own size (especially its height given a certain width). Also, the HTML capabilities make it easy to add font styles and possibly inline images. In my own code I did end up implementing my own class (derived from QTextDocument) to support more content and work around an annoying bug.

              // listviewdelegate.h
              #ifndef LISTVIEWDELEGATE_H_
              #define LISTVIEWDELEGATE_H_
              
              #include <QAbstractItemDelegate>
              #include <QPainter>
              
              class ListViewDelegate : public QAbstractItemDelegate
              {
                int d_radius;
                int d_toppadding;
                int d_bottompadding;
                int d_leftpadding;
                int d_rightpadding;
                int d_verticalmargin;
                int d_horizontalmargin;
                int d_pointerwidth;
                int d_pointerheight;
                float d_widthfraction;
               public:
                inline ListViewDelegate(QObject *parent = nullptr);
               protected:
                inline void paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const;
                inline QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const;
              };
              
              inline ListViewDelegate::ListViewDelegate(QObject *parent)
                :
                QAbstractItemDelegate(parent),
                d_radius(5),
                d_toppadding(5),
                d_bottompadding(3),
                d_leftpadding(5),
                d_rightpadding(5),
                d_verticalmargin(15),
                d_horizontalmargin(10),
                d_pointerwidth(10),
                d_pointerheight(17),
                d_widthfraction(.7)
              {}
              
              inline void ListViewDelegate::paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const
              {
                QTextDocument bodydoc;
                QTextOption textOption(bodydoc.defaultTextOption());
                textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                bodydoc.setDefaultTextOption(textOption);
                bodydoc.setDefaultFont(QFont("Roboto", 12));
                QString bodytext(index.data(Qt::DisplayRole).toString());
                bodydoc.setHtml(bodytext);
              
                qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                bodydoc.setTextWidth(contentswidth);
                qreal bodyheight = bodydoc.size().height();
              
                bool outgoing = index.data(Qt::UserRole + 1).toString() == "Outgoing";
              
                painter->save();
                painter->setRenderHint(QPainter::Antialiasing);
              
                // uncomment to see the area provided to paint this item
                //painter->drawRect(option.rect);
              
                painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
              
                // background color for chat bubble
                QColor bgcolor("#DD1212");
                if (outgoing)
                  bgcolor = "#DDDDDD";
              
                // create chat bubble
                QPainterPath pointie;
              
                // left bottom
                pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
              
                // right bottom
                pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius,
                               bodyheight + d_toppadding + d_bottompadding);
                pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius,
                              bodyheight + d_toppadding + d_bottompadding - 2 * d_radius,
                              2 * d_radius, 2 * d_radius, 270, 90);
              
                // right top
                pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding, 0 + d_radius);
                pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius, 0,
                              2 * d_radius, 2 * d_radius, 0, 90);
              
                // left top
                pointie.lineTo(0 + d_pointerwidth + d_radius, 0);
                pointie.arcTo(0 + d_pointerwidth, 0, 2 * d_radius, 2 * d_radius, 90, 90);
              
                // left bottom almost (here is the pointie)
                pointie.lineTo(0 + d_pointerwidth, bodyheight + d_toppadding + d_bottompadding - d_pointerheight);
                pointie.closeSubpath();
              
                // rotate bubble for outgoing messages
                if (outgoing)
                {
                  painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                  painter->translate(pointie.boundingRect().center());
                  painter->rotate(180);
                  painter->translate(-pointie.boundingRect().center());
                }
              
                // now paint it!
                painter->setPen(QPen(bgcolor));
                painter->drawPath(pointie);
                painter->fillPath(pointie, QBrush(bgcolor));
              
                // rotate back or painter is going to paint the text rotated...
                if (outgoing)
                {
                  painter->translate(pointie.boundingRect().center());
                  painter->rotate(-180);
                  painter->translate(-pointie.boundingRect().center());
                }
              
                // set text color used to draw message body
                QAbstractTextDocumentLayout::PaintContext ctx;
                if (outgoing)
                  ctx.palette.setColor(QPalette::Text, QColor("black"));
                else
                  ctx.palette.setColor(QPalette::Text, QColor("white"));
              
                // draw body text
                painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                bodydoc.documentLayout()->draw(painter, ctx);
              
                painter->restore();
              }
              
              inline QSize ListViewDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const
              {
                QTextDocument bodydoc;
                QTextOption textOption(bodydoc.defaultTextOption());
                textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                bodydoc.setDefaultTextOption(textOption);
                bodydoc.setDefaultFont(QFont("Roboto", 12));
                QString bodytext(index.data(Qt::DisplayRole).toString());
                bodydoc.setHtml(bodytext);
              
                // the width of the contents are the (a fraction of the window width) minus (margins + padding + width of the bubble's tail)
                qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
              
                // set this available width on the text document
                bodydoc.setTextWidth(contentswidth);
              
                QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                           bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1); // I dont remember why +1, haha, might not be necessary
              
                if (index.row() == 0) // have extra margin at top of first item
                  size += QSize(0, d_verticalmargin);
              
                return size;
              }
              
              #endif
              

              It should compile with

              qmake -project QT+=widgets
              qmake
              make
              
              T 1 Reply Last reply 21 Apr 2019, 20:05
              5
              • B bepaald
                17 Jan 2019, 17:47

                @Taytoo
                Ok, I got around to creating a little example. It shows just a normal QListView, but painting the data in its model with a custom delegate. This delegate has two main functions: first it needs to report the size in the view an item is going to need to be painted. Second, it needs to paint.

                The main file just sets up some dummy data in a standard QStandardItemModel. Make sure you add any data to the model that your delegate might need to paint the item properly. In this example I only added an "outgoing" or "incoming" datafield to get some different messages, but you may need a ton more dataroles (I did!). Then it creates a pretty standard QListView. The only custom stuff is the ListViewDelegate set on the view using QListView::setItemDelegate().

                //main.cc
                
                #include <QtWidgets>
                
                #include "listviewdelegate.h"
                
                int main(int argc, char *argv[])
                {
                  QApplication app(argc, argv);
                
                  // create some data and put it in a model
                  QStandardItemModel mymodel;
                  QStandardItem *item1 = new QStandardItem("This is item one");
                  item1->setData("Incoming", Qt::UserRole + 1);
                  mymodel.appendRow(item1);
                  QStandardItem *item2 = new QStandardItem("This is item two, it is a very long item, but it's not the item's fault, it is me typing all this text.");
                  item2->setData("Outgoing", Qt::UserRole + 1);
                  mymodel.appendRow(item2);
                  QStandardItem *item3 = new QStandardItem("This is the third item");
                  item3->setData("Incoming", Qt::UserRole + 1);
                  mymodel.appendRow(item3);
                
                  // create a view and set our data
                  QListView listview;
                  listview.setResizeMode(QListView::Adjust);
                  listview.setWordWrap(true);
                  listview.setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
                  listview.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
                  listview.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
                  listview.setModel(&mymodel);
                  listview.setMinimumSize(200,350);
                
                  // NOW tell the view to rely on out custom delegate for drawing
                  listview.setItemDelegate(new ListViewDelegate());
                
                  // show it
                  listview.show();
                
                  return app.exec();
                }
                

                As said, the delegate just gives size hints and paints. You can paint anything you want in the items, just make sure to also update the sizeHint() for any new elements you're painting. I am using a QTextDocument to store the body text, as it can easily report its own size (especially its height given a certain width). Also, the HTML capabilities make it easy to add font styles and possibly inline images. In my own code I did end up implementing my own class (derived from QTextDocument) to support more content and work around an annoying bug.

                // listviewdelegate.h
                #ifndef LISTVIEWDELEGATE_H_
                #define LISTVIEWDELEGATE_H_
                
                #include <QAbstractItemDelegate>
                #include <QPainter>
                
                class ListViewDelegate : public QAbstractItemDelegate
                {
                  int d_radius;
                  int d_toppadding;
                  int d_bottompadding;
                  int d_leftpadding;
                  int d_rightpadding;
                  int d_verticalmargin;
                  int d_horizontalmargin;
                  int d_pointerwidth;
                  int d_pointerheight;
                  float d_widthfraction;
                 public:
                  inline ListViewDelegate(QObject *parent = nullptr);
                 protected:
                  inline void paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const;
                  inline QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const;
                };
                
                inline ListViewDelegate::ListViewDelegate(QObject *parent)
                  :
                  QAbstractItemDelegate(parent),
                  d_radius(5),
                  d_toppadding(5),
                  d_bottompadding(3),
                  d_leftpadding(5),
                  d_rightpadding(5),
                  d_verticalmargin(15),
                  d_horizontalmargin(10),
                  d_pointerwidth(10),
                  d_pointerheight(17),
                  d_widthfraction(.7)
                {}
                
                inline void ListViewDelegate::paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const
                {
                  QTextDocument bodydoc;
                  QTextOption textOption(bodydoc.defaultTextOption());
                  textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                  bodydoc.setDefaultTextOption(textOption);
                  bodydoc.setDefaultFont(QFont("Roboto", 12));
                  QString bodytext(index.data(Qt::DisplayRole).toString());
                  bodydoc.setHtml(bodytext);
                
                  qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                  bodydoc.setTextWidth(contentswidth);
                  qreal bodyheight = bodydoc.size().height();
                
                  bool outgoing = index.data(Qt::UserRole + 1).toString() == "Outgoing";
                
                  painter->save();
                  painter->setRenderHint(QPainter::Antialiasing);
                
                  // uncomment to see the area provided to paint this item
                  //painter->drawRect(option.rect);
                
                  painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                
                  // background color for chat bubble
                  QColor bgcolor("#DD1212");
                  if (outgoing)
                    bgcolor = "#DDDDDD";
                
                  // create chat bubble
                  QPainterPath pointie;
                
                  // left bottom
                  pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
                
                  // right bottom
                  pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius,
                                 bodyheight + d_toppadding + d_bottompadding);
                  pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius,
                                bodyheight + d_toppadding + d_bottompadding - 2 * d_radius,
                                2 * d_radius, 2 * d_radius, 270, 90);
                
                  // right top
                  pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding, 0 + d_radius);
                  pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius, 0,
                                2 * d_radius, 2 * d_radius, 0, 90);
                
                  // left top
                  pointie.lineTo(0 + d_pointerwidth + d_radius, 0);
                  pointie.arcTo(0 + d_pointerwidth, 0, 2 * d_radius, 2 * d_radius, 90, 90);
                
                  // left bottom almost (here is the pointie)
                  pointie.lineTo(0 + d_pointerwidth, bodyheight + d_toppadding + d_bottompadding - d_pointerheight);
                  pointie.closeSubpath();
                
                  // rotate bubble for outgoing messages
                  if (outgoing)
                  {
                    painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                    painter->translate(pointie.boundingRect().center());
                    painter->rotate(180);
                    painter->translate(-pointie.boundingRect().center());
                  }
                
                  // now paint it!
                  painter->setPen(QPen(bgcolor));
                  painter->drawPath(pointie);
                  painter->fillPath(pointie, QBrush(bgcolor));
                
                  // rotate back or painter is going to paint the text rotated...
                  if (outgoing)
                  {
                    painter->translate(pointie.boundingRect().center());
                    painter->rotate(-180);
                    painter->translate(-pointie.boundingRect().center());
                  }
                
                  // set text color used to draw message body
                  QAbstractTextDocumentLayout::PaintContext ctx;
                  if (outgoing)
                    ctx.palette.setColor(QPalette::Text, QColor("black"));
                  else
                    ctx.palette.setColor(QPalette::Text, QColor("white"));
                
                  // draw body text
                  painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                  bodydoc.documentLayout()->draw(painter, ctx);
                
                  painter->restore();
                }
                
                inline QSize ListViewDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const
                {
                  QTextDocument bodydoc;
                  QTextOption textOption(bodydoc.defaultTextOption());
                  textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                  bodydoc.setDefaultTextOption(textOption);
                  bodydoc.setDefaultFont(QFont("Roboto", 12));
                  QString bodytext(index.data(Qt::DisplayRole).toString());
                  bodydoc.setHtml(bodytext);
                
                  // the width of the contents are the (a fraction of the window width) minus (margins + padding + width of the bubble's tail)
                  qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                
                  // set this available width on the text document
                  bodydoc.setTextWidth(contentswidth);
                
                  QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                             bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1); // I dont remember why +1, haha, might not be necessary
                
                  if (index.row() == 0) // have extra margin at top of first item
                    size += QSize(0, d_verticalmargin);
                
                  return size;
                }
                
                #endif
                

                It should compile with

                qmake -project QT+=widgets
                qmake
                make
                
                T Offline
                T Offline
                Taytoo
                wrote on 21 Apr 2019, 20:05 last edited by
                #7

                @bepaald

                Sorry, I just saw that you responded with a solution. It works really well :)

                One issue though, since the text is painted, it doesn't allow selecting and copying text from the chat bubble. How did you solve that limitation?

                S 1 Reply Last reply 1 Mar 2024, 14:13
                0
                • S Offline
                  S Offline
                  steveq
                  wrote on 1 Jun 2019, 02:39 last edited by
                  #8

                  Hi @bepaald ,

                  Firstly, thank you for your delegate. It works really well! I have added several more UserRoles and modified your code accordingly. I'm really happy with the way it is working.

                  A question though, embedding html works really well, as does embedding inline images using the html tag <img src= etc.

                  A can also embed a hyperlink, however the link is not clickable, probably because it painted into the bubble.

                  Do you have a suggestion as to how I can get around this please?

                  Thanks so much,

                  Steve Q.

                  1 Reply Last reply
                  0
                  • S Offline
                    S Offline
                    SGaist
                    Lifetime Qt Champion
                    wrote on 1 Jun 2019, 20:22 last edited by
                    #9

                    Hi,

                    You need to add custom handling of mouse click and check whether what's under it matches the position of an URL and then act accordingly. For example use QDesktopServices::openUrl to open it.

                    Interested in AI ? www.idiap.ch
                    Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                    S 1 Reply Last reply 2 Jun 2019, 14:14
                    1
                    • S SGaist
                      1 Jun 2019, 20:22

                      Hi,

                      You need to add custom handling of mouse click and check whether what's under it matches the position of an URL and then act accordingly. For example use QDesktopServices::openUrl to open it.

                      S Offline
                      S Offline
                      steveq
                      wrote on 2 Jun 2019, 14:14 last edited by
                      #10

                      @SGaist thank you, it has worked a treat!

                      I appreciate your help.

                      Steve Q.

                      1 Reply Last reply
                      0
                      • T Taytoo
                        21 Apr 2019, 20:05

                        @bepaald

                        Sorry, I just saw that you responded with a solution. It works really well :)

                        One issue though, since the text is painted, it doesn't allow selecting and copying text from the chat bubble. How did you solve that limitation?

                        S Offline
                        S Offline
                        steveq
                        wrote on 1 Mar 2024, 14:13 last edited by steveq 3 Feb 2024, 01:03
                        #11

                        @Taytoo
                        As requested, here is my modified chatbubbledelegate.h:

                        #ifndef CHATBUBBLEDELEGATE_H
                        #define CHATBUBBLEDELEGATE_H
                        
                        #include <QAbstractItemDelegate>
                        #include <QPainter>
                        #include <QTextDocument>
                        #include <QAbstractTextDocumentLayout>
                        
                        class ChatBubbleDelegate : public QAbstractItemDelegate
                        {
                          int d_radius;
                          int d_toppadding;
                          int d_bottompadding;
                          int d_leftpadding;
                          int d_rightpadding;
                          int d_verticalmargin;
                          int d_horizontalmargin;
                          int d_pointerwidth;
                          int d_pointerheight;
                          float d_widthfraction;
                         public:
                          inline ChatBubbleDelegate(QObject *parent = nullptr);
                         protected:
                          inline void paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const;
                          inline QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const;
                        };
                        
                        inline ChatBubbleDelegate::ChatBubbleDelegate(QObject *parent)
                          :
                          QAbstractItemDelegate(parent),
                          d_radius(5),
                          d_toppadding(2),
                          d_bottompadding(1),
                          d_leftpadding(5),
                          d_rightpadding(5),
                          d_verticalmargin(10),
                          d_horizontalmargin(10),
                          d_pointerwidth(10),
                          d_pointerheight(17),
                          d_widthfraction(.7)
                        {}
                        
                        inline void ChatBubbleDelegate::paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const
                        {
                        
                        //	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                        //	{
                        //		bool outgoing = index.data(Qt::UserRole + 3).toString() == "Outgoing";
                        //		QTextDocument bodydoc;
                        //		QTextOption textOption(bodydoc.defaultTextOption());
                        //		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                        //		bodydoc.setDefaultTextOption(textOption);
                        //#if defined (Q_OS_OSX)
                        //		bodydoc.setDefaultFont(QFont("Roboto", 13));
                        //#elif defined (Q_OS_WIN)
                        //		bodydoc.setDefaultFont(QFont("Roboto", 8));
                        //#endif
                        //		QString bodytext(index.data(Qt::DisplayRole).toString());
                        //		bodydoc.setHtml(bodytext);
                        
                        //		painter->save();
                        //		painter->setRenderHint(QPainter::Antialiasing);
                        
                        //		painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                        ////			  painter->drawRect(option.rect);
                        
                        //		QAbstractTextDocumentLayout::PaintContext ctx;
                        
                        //		ctx.palette.setColor(QPalette::Text, QColor("grey"));
                        
                        //		// draw body text
                        //		painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                        
                        //		bodydoc.documentLayout()->draw(painter, ctx);
                        
                        //		painter->restore();
                        //	}
                        
                        	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                        	{
                        		bool outgoing = index.data(Qt::UserRole + 3).toString() == "Outgoing";
                        
                        		QTextDocument bodydoc;
                        		QTextOption textOption(bodydoc.defaultTextOption());
                        		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                        		bodydoc.setDefaultTextOption(textOption);
                        #if defined (Q_OS_OSX)
                        		bodydoc.setDefaultFont(QFont("Roboto", 13));
                        #elif defined (Q_OS_WIN)
                        		bodydoc.setDefaultFont(QFont("Roboto", 8));
                        #endif
                        		QString bodytext(index.data(Qt::DisplayRole).toString());
                        		bodydoc.setHtml(bodytext);
                        
                        		qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                        		bodydoc.setTextWidth(contentswidth);
                        
                        		qreal bodyheight = bodydoc.size().height();
                        
                        		painter->save();
                        		painter->setRenderHint(QPainter::Antialiasing);
                        
                        		painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                        
                        		// Start drawing the chat bubble, but we don't use it. This is just to get the correct horizontal offset position for the
                        		// name and/or timestamp text
                        		QPainterPath pointie;
                        
                        		// left bottom
                        		pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
                        
                        		// right bottom
                        		pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius, bodyheight + d_toppadding + d_bottompadding);
                        
                        		pointie.closeSubpath();
                        
                        		if (outgoing) painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                        
                        		// uncomment to see the area provided to paint this item
                        		// painter->drawRect(option.rect);
                        
                        		QAbstractTextDocumentLayout::PaintContext ctx;
                        
                        		ctx.palette.setColor(QPalette::Text, QColor("grey"));
                        
                        		// draw body text
                        		painter->translate((outgoing ? 0 : d_pointerwidth + d_leftpadding), 0);
                        
                        		bodydoc.documentLayout()->draw(painter, ctx);
                        
                        		painter->restore();
                        	}
                        
                        	else
                        	{
                        		QTextDocument bodydoc;
                        		QTextOption textOption(bodydoc.defaultTextOption());
                        		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                        		bodydoc.setDefaultTextOption(textOption);
                        #if defined (Q_OS_OSX)
                        		bodydoc.setDefaultFont(QFont("Roboto", 14));
                        #elif defined (Q_OS_WIN)
                        		bodydoc.setDefaultFont(QFont("Roboto", 9));
                        #endif
                        		QString bodytext(index.data(Qt::DisplayRole).toString());
                        		bodydoc.setHtml(bodytext);
                        
                        		if ( index.data(Qt::UserRole + 1).toString().contains("Status") )
                        		{
                        			painter->save();
                        			painter->setRenderHint(QPainter::Antialiasing);
                        
                        			painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                        			//painter->drawRect(option.rect);
                        
                        			QAbstractTextDocumentLayout::PaintContext ctx;
                        
                        			ctx.palette.setColor(QPalette::Text, QColor("grey"));
                        
                        			// draw body text
                        			painter->translate((false ? 0 : d_pointerwidth) + d_leftpadding, 0);
                        
                        			bodydoc.documentLayout()->draw(painter, ctx);
                        
                        			painter->restore();
                        		}
                        
                        		else
                        		{
                        			qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                        			bodydoc.setTextWidth(contentswidth);
                        
                        			qreal bodyheight = bodydoc.size().height();
                        
                        			bool outgoing = index.data(Qt::UserRole + 1).toString() == "Outgoing";
                        			QString colour = index.data(Qt::UserRole + 2).toString();
                        
                        			painter->save();
                        			painter->setRenderHint(QPainter::Antialiasing);
                        
                        			// uncomment to see the area provided to paint this item
                        			//painter->drawRect(option.rect);
                        
                        			painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                        
                        			// background color for chat bubble
                        			QColor bgcolor(colour);
                        			//  if (outgoing)
                        			//	bgcolor = "#DDDDDD";
                        
                        			// create chat bubble
                        			QPainterPath pointie;
                        
                        			// left bottom
                        			pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
                        
                        			// right bottom
                        			pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius,
                        						 bodyheight + d_toppadding + d_bottompadding);
                        			pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius,
                        						bodyheight + d_toppadding + d_bottompadding - 2 * d_radius,
                        						2 * d_radius, 2 * d_radius, 270, 90);
                        
                        			// right top
                        			pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding, 0 + d_radius);
                        			pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius, 0,
                        						2 * d_radius, 2 * d_radius, 0, 90);
                        
                        			// left top
                        			pointie.lineTo(0 + d_pointerwidth + d_radius, 0);
                        			pointie.arcTo(0 + d_pointerwidth, 0, 2 * d_radius, 2 * d_radius, 90, 90);
                        
                        			// left bottom almost (here is the pointie)
                        			pointie.lineTo(0 + d_pointerwidth, bodyheight + d_toppadding + d_bottompadding - d_pointerheight);
                        			pointie.closeSubpath();
                        
                        			// rotate bubble for outgoing messages
                        			if (outgoing)
                        			{
                        				painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                        				painter->translate(pointie.boundingRect().center());
                        				painter->rotate(180);
                        				painter->translate(-pointie.boundingRect().center());
                        			}
                        
                        			// now paint it!
                        			painter->setPen(QPen(bgcolor));
                        			painter->drawPath(pointie);
                        			painter->fillPath(pointie, QBrush(bgcolor));
                        
                        			// rotate back or painter is going to paint the text rotated...
                        			if (outgoing)
                        			{
                        				painter->translate(pointie.boundingRect().center());
                        				painter->rotate(-180);
                        				painter->translate(-pointie.boundingRect().center());
                        			}
                        
                        			// set text color used to draw message body
                        			QAbstractTextDocumentLayout::PaintContext ctx;
                        
                        			ctx.palette.setColor(QPalette::Text, QColor("black"));
                        
                        			// draw body text
                        			painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                        			bodydoc.documentLayout()->draw(painter, ctx);
                        
                        			painter->restore();
                        		}
                        	}
                        }
                        
                        inline QSize ChatBubbleDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const
                        {
                        	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                        	{
                        		QTextDocument bodydoc;
                        		QTextOption textOption(bodydoc.defaultTextOption());
                        		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                        		bodydoc.setDefaultTextOption(textOption);
                        #if defined (Q_OS_OSX)
                        		bodydoc.setDefaultFont(QFont("Roboto", 13));
                        #elif defined (Q_OS_WIN)
                        		bodydoc.setDefaultFont(QFont("Roboto", 8));
                        #endif
                        		QString bodytext(index.data(Qt::DisplayRole).toString());
                        		bodydoc.setHtml(bodytext);
                        
                        		QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                        			 bodydoc.size().height() + d_bottompadding);
                        
                        		return size;
                        	}
                        
                        	else
                        	{
                        		QTextDocument bodydoc;
                        		QTextOption textOption(bodydoc.defaultTextOption());
                        		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                        		bodydoc.setDefaultTextOption(textOption);
                        #if defined (Q_OS_OSX)
                        		bodydoc.setDefaultFont(QFont("Roboto", 14));
                        #elif defined (Q_OS_WIN)
                        		bodydoc.setDefaultFont(QFont("Roboto", 9));
                        #endif
                        		QString bodytext(index.data(Qt::DisplayRole).toString());
                        		bodydoc.setHtml(bodytext);
                        
                        		// the width of the contents are the (a fraction of the window width) minus (margins + padding + width of the bubble's tail)
                        		qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                        
                        		// set this available width on the text document
                        		bodydoc.setTextWidth(contentswidth);
                        
                        		if ( index.data(Qt::UserRole + 1).toString().contains("Status") )
                        		{
                        			QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                        				 bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin);
                        
                        			return size;
                        		}
                        
                        		else
                        		{
                        			if ( index.data(Qt::UserRole + 1).toString().contains("Outgoing") )
                        			{
                        				QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                        						bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1);
                        
                        				return size;
                        			}
                        
                        			else
                        			{
                        				QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                        						bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1);
                        
                        				return size;
                        			}
                        		}
                        	}
                        }
                        
                        #endif // CHATBUBBLEDELEGATE_H
                        
                        

                        And this is my function for writing a message to the chat bubble:

                        void ChatTab::addMessage(QString userColour, QString username, QString message, int itemType, QString itemData)
                        {
                        	QString roleMode;
                        
                        	QColor color(userColour), newColor = color;
                        
                        	int lightnessAmount = 100;
                        
                        	// Convert carriage returns to html carriage returns so they are displayed properly
                        	message.replace("\n", "<br>");
                        
                        	// Crappy way of doing it, but make sure the user colour has a lightness colour of 230 or greater.
                        	// 230 is an arbitrary colour I have chosen
                        	for ( ;; )
                        	{
                        		if ( newColor.lightness() < 230 )
                        		{
                        			lightnessAmount += 10;
                        
                        			newColor = color.lighter(lightnessAmount);
                        		}
                        
                        		else break;
                        	}
                        
                        	// Print the senders name and or time stamp
                        	QString statusText;
                        
                        	if ( m_settings->username == username ) roleMode = "Outgoing";
                        	else roleMode = "Incoming";
                        
                        	if ( m_membersList->count() > 1  && roleMode == "Incoming" ) statusText = username + " ";
                        
                        	statusText += QTime::currentTime().toString( "HH:mm:ss");
                        
                        	struct ChatListItem *cli;
                        
                        	QStandardItem *item1 = new QStandardItem(statusText);
                        
                        	item1->setData(roleMode, Qt::UserRole + 3);
                        
                        	cli = new struct ChatListItem;
                        
                        	cli->itemData = "";
                        	cli->itemText = username;
                        	cli->itemType = CHATLISTITEMTYPE_STATUSMESSAGE;
                        
                        	item1->setData(  QVariant(QVariant::fromValue(static_cast<void*>(cli))), Qt::UserRole + 20 );
                        
                        	m_mymodel.appendRow(item1);
                        
                        	// Print the message
                        	if ( m_settings->username == username ) roleMode = "Outgoing";
                        	else roleMode = "Incoming";
                        
                        	QStandardItem *item2 = new QStandardItem(message);
                        
                        	item2->setData(roleMode, Qt::UserRole + 1);
                        
                        	item2->setData(newColor.name(), Qt::UserRole + 2);
                        
                        	item2->setData("", Qt::UserRole + 3);
                        
                        	cli = new struct ChatListItem;
                        
                        	cli->itemData = itemData;
                        	cli->itemText = message;
                        	cli->itemType = itemType;
                        
                        	item2->setData(  QVariant(QVariant::fromValue(static_cast<void*>(cli))), Qt::UserRole + 20 );
                        
                        	m_mymodel.appendRow(item2);
                        
                        	m_chatList->scrollToBottom();
                        
                        	addToTranscript( username, message );
                        }
                        

                        As I said in my email, it was a while ago when I wrote this. If you have any questions, let me know and I'll try and figure out what I was doing! Lol

                        Small Talk.png

                        This is a screen grab of my chat window. I couldn't grab it with the context menu being displayed, but each bubble has its own context menu which do various things depending on the content of the bubble. Such as downloading an image and displaying it, opening a web page, or copying the bubble text to the clipboard. HTML codes can also be embedded into the bubble and display correctly.
                        I hope all this helps.
                        Steve Q.

                        T 1 Reply Last reply 4 Mar 2024, 17:40
                        0
                        • S steveq
                          1 Mar 2024, 14:13

                          @Taytoo
                          As requested, here is my modified chatbubbledelegate.h:

                          #ifndef CHATBUBBLEDELEGATE_H
                          #define CHATBUBBLEDELEGATE_H
                          
                          #include <QAbstractItemDelegate>
                          #include <QPainter>
                          #include <QTextDocument>
                          #include <QAbstractTextDocumentLayout>
                          
                          class ChatBubbleDelegate : public QAbstractItemDelegate
                          {
                            int d_radius;
                            int d_toppadding;
                            int d_bottompadding;
                            int d_leftpadding;
                            int d_rightpadding;
                            int d_verticalmargin;
                            int d_horizontalmargin;
                            int d_pointerwidth;
                            int d_pointerheight;
                            float d_widthfraction;
                           public:
                            inline ChatBubbleDelegate(QObject *parent = nullptr);
                           protected:
                            inline void paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const;
                            inline QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const;
                          };
                          
                          inline ChatBubbleDelegate::ChatBubbleDelegate(QObject *parent)
                            :
                            QAbstractItemDelegate(parent),
                            d_radius(5),
                            d_toppadding(2),
                            d_bottompadding(1),
                            d_leftpadding(5),
                            d_rightpadding(5),
                            d_verticalmargin(10),
                            d_horizontalmargin(10),
                            d_pointerwidth(10),
                            d_pointerheight(17),
                            d_widthfraction(.7)
                          {}
                          
                          inline void ChatBubbleDelegate::paint(QPainter *painter, QStyleOptionViewItem const &option, QModelIndex const &index) const
                          {
                          
                          //	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                          //	{
                          //		bool outgoing = index.data(Qt::UserRole + 3).toString() == "Outgoing";
                          //		QTextDocument bodydoc;
                          //		QTextOption textOption(bodydoc.defaultTextOption());
                          //		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                          //		bodydoc.setDefaultTextOption(textOption);
                          //#if defined (Q_OS_OSX)
                          //		bodydoc.setDefaultFont(QFont("Roboto", 13));
                          //#elif defined (Q_OS_WIN)
                          //		bodydoc.setDefaultFont(QFont("Roboto", 8));
                          //#endif
                          //		QString bodytext(index.data(Qt::DisplayRole).toString());
                          //		bodydoc.setHtml(bodytext);
                          
                          //		painter->save();
                          //		painter->setRenderHint(QPainter::Antialiasing);
                          
                          //		painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                          ////			  painter->drawRect(option.rect);
                          
                          //		QAbstractTextDocumentLayout::PaintContext ctx;
                          
                          //		ctx.palette.setColor(QPalette::Text, QColor("grey"));
                          
                          //		// draw body text
                          //		painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                          
                          //		bodydoc.documentLayout()->draw(painter, ctx);
                          
                          //		painter->restore();
                          //	}
                          
                          	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                          	{
                          		bool outgoing = index.data(Qt::UserRole + 3).toString() == "Outgoing";
                          
                          		QTextDocument bodydoc;
                          		QTextOption textOption(bodydoc.defaultTextOption());
                          		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                          		bodydoc.setDefaultTextOption(textOption);
                          #if defined (Q_OS_OSX)
                          		bodydoc.setDefaultFont(QFont("Roboto", 13));
                          #elif defined (Q_OS_WIN)
                          		bodydoc.setDefaultFont(QFont("Roboto", 8));
                          #endif
                          		QString bodytext(index.data(Qt::DisplayRole).toString());
                          		bodydoc.setHtml(bodytext);
                          
                          		qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                          		bodydoc.setTextWidth(contentswidth);
                          
                          		qreal bodyheight = bodydoc.size().height();
                          
                          		painter->save();
                          		painter->setRenderHint(QPainter::Antialiasing);
                          
                          		painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                          
                          		// Start drawing the chat bubble, but we don't use it. This is just to get the correct horizontal offset position for the
                          		// name and/or timestamp text
                          		QPainterPath pointie;
                          
                          		// left bottom
                          		pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
                          
                          		// right bottom
                          		pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius, bodyheight + d_toppadding + d_bottompadding);
                          
                          		pointie.closeSubpath();
                          
                          		if (outgoing) painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                          
                          		// uncomment to see the area provided to paint this item
                          		// painter->drawRect(option.rect);
                          
                          		QAbstractTextDocumentLayout::PaintContext ctx;
                          
                          		ctx.palette.setColor(QPalette::Text, QColor("grey"));
                          
                          		// draw body text
                          		painter->translate((outgoing ? 0 : d_pointerwidth + d_leftpadding), 0);
                          
                          		bodydoc.documentLayout()->draw(painter, ctx);
                          
                          		painter->restore();
                          	}
                          
                          	else
                          	{
                          		QTextDocument bodydoc;
                          		QTextOption textOption(bodydoc.defaultTextOption());
                          		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                          		bodydoc.setDefaultTextOption(textOption);
                          #if defined (Q_OS_OSX)
                          		bodydoc.setDefaultFont(QFont("Roboto", 14));
                          #elif defined (Q_OS_WIN)
                          		bodydoc.setDefaultFont(QFont("Roboto", 9));
                          #endif
                          		QString bodytext(index.data(Qt::DisplayRole).toString());
                          		bodydoc.setHtml(bodytext);
                          
                          		if ( index.data(Qt::UserRole + 1).toString().contains("Status") )
                          		{
                          			painter->save();
                          			painter->setRenderHint(QPainter::Antialiasing);
                          
                          			painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                          			//painter->drawRect(option.rect);
                          
                          			QAbstractTextDocumentLayout::PaintContext ctx;
                          
                          			ctx.palette.setColor(QPalette::Text, QColor("grey"));
                          
                          			// draw body text
                          			painter->translate((false ? 0 : d_pointerwidth) + d_leftpadding, 0);
                          
                          			bodydoc.documentLayout()->draw(painter, ctx);
                          
                          			painter->restore();
                          		}
                          
                          		else
                          		{
                          			qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                          			bodydoc.setTextWidth(contentswidth);
                          
                          			qreal bodyheight = bodydoc.size().height();
                          
                          			bool outgoing = index.data(Qt::UserRole + 1).toString() == "Outgoing";
                          			QString colour = index.data(Qt::UserRole + 2).toString();
                          
                          			painter->save();
                          			painter->setRenderHint(QPainter::Antialiasing);
                          
                          			// uncomment to see the area provided to paint this item
                          			//painter->drawRect(option.rect);
                          
                          			painter->translate(option.rect.left() + d_horizontalmargin, option.rect.top() + ((index.row() == 0) ? d_verticalmargin : 0));
                          
                          			// background color for chat bubble
                          			QColor bgcolor(colour);
                          			//  if (outgoing)
                          			//	bgcolor = "#DDDDDD";
                          
                          			// create chat bubble
                          			QPainterPath pointie;
                          
                          			// left bottom
                          			pointie.moveTo(0, bodyheight + d_toppadding + d_bottompadding);
                          
                          			// right bottom
                          			pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - d_radius,
                          						 bodyheight + d_toppadding + d_bottompadding);
                          			pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius,
                          						bodyheight + d_toppadding + d_bottompadding - 2 * d_radius,
                          						2 * d_radius, 2 * d_radius, 270, 90);
                          
                          			// right top
                          			pointie.lineTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding, 0 + d_radius);
                          			pointie.arcTo(0 + contentswidth + d_pointerwidth + d_leftpadding + d_rightpadding - 2 * d_radius, 0,
                          						2 * d_radius, 2 * d_radius, 0, 90);
                          
                          			// left top
                          			pointie.lineTo(0 + d_pointerwidth + d_radius, 0);
                          			pointie.arcTo(0 + d_pointerwidth, 0, 2 * d_radius, 2 * d_radius, 90, 90);
                          
                          			// left bottom almost (here is the pointie)
                          			pointie.lineTo(0 + d_pointerwidth, bodyheight + d_toppadding + d_bottompadding - d_pointerheight);
                          			pointie.closeSubpath();
                          
                          			// rotate bubble for outgoing messages
                          			if (outgoing)
                          			{
                          				painter->translate(option.rect.width() - pointie.boundingRect().width() - d_horizontalmargin - d_pointerwidth, 0);
                          				painter->translate(pointie.boundingRect().center());
                          				painter->rotate(180);
                          				painter->translate(-pointie.boundingRect().center());
                          			}
                          
                          			// now paint it!
                          			painter->setPen(QPen(bgcolor));
                          			painter->drawPath(pointie);
                          			painter->fillPath(pointie, QBrush(bgcolor));
                          
                          			// rotate back or painter is going to paint the text rotated...
                          			if (outgoing)
                          			{
                          				painter->translate(pointie.boundingRect().center());
                          				painter->rotate(-180);
                          				painter->translate(-pointie.boundingRect().center());
                          			}
                          
                          			// set text color used to draw message body
                          			QAbstractTextDocumentLayout::PaintContext ctx;
                          
                          			ctx.palette.setColor(QPalette::Text, QColor("black"));
                          
                          			// draw body text
                          			painter->translate((outgoing ? 0 : d_pointerwidth) + d_leftpadding, 0);
                          			bodydoc.documentLayout()->draw(painter, ctx);
                          
                          			painter->restore();
                          		}
                          	}
                          }
                          
                          inline QSize ChatBubbleDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const
                          {
                          	if (!index.data(Qt::UserRole + 3).toString().isEmpty())
                          	{
                          		QTextDocument bodydoc;
                          		QTextOption textOption(bodydoc.defaultTextOption());
                          		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                          		bodydoc.setDefaultTextOption(textOption);
                          #if defined (Q_OS_OSX)
                          		bodydoc.setDefaultFont(QFont("Roboto", 13));
                          #elif defined (Q_OS_WIN)
                          		bodydoc.setDefaultFont(QFont("Roboto", 8));
                          #endif
                          		QString bodytext(index.data(Qt::DisplayRole).toString());
                          		bodydoc.setHtml(bodytext);
                          
                          		QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                          			 bodydoc.size().height() + d_bottompadding);
                          
                          		return size;
                          	}
                          
                          	else
                          	{
                          		QTextDocument bodydoc;
                          		QTextOption textOption(bodydoc.defaultTextOption());
                          		textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
                          		bodydoc.setDefaultTextOption(textOption);
                          #if defined (Q_OS_OSX)
                          		bodydoc.setDefaultFont(QFont("Roboto", 14));
                          #elif defined (Q_OS_WIN)
                          		bodydoc.setDefaultFont(QFont("Roboto", 9));
                          #endif
                          		QString bodytext(index.data(Qt::DisplayRole).toString());
                          		bodydoc.setHtml(bodytext);
                          
                          		// the width of the contents are the (a fraction of the window width) minus (margins + padding + width of the bubble's tail)
                          		qreal contentswidth = option.rect.width() * d_widthfraction - d_horizontalmargin - d_pointerwidth - d_leftpadding - d_rightpadding;
                          
                          		// set this available width on the text document
                          		bodydoc.setTextWidth(contentswidth);
                          
                          		if ( index.data(Qt::UserRole + 1).toString().contains("Status") )
                          		{
                          			QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                          				 bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin);
                          
                          			return size;
                          		}
                          
                          		else
                          		{
                          			if ( index.data(Qt::UserRole + 1).toString().contains("Outgoing") )
                          			{
                          				QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                          						bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1);
                          
                          				return size;
                          			}
                          
                          			else
                          			{
                          				QSize size(bodydoc.idealWidth() + d_horizontalmargin + d_pointerwidth + d_leftpadding + d_rightpadding,
                          						bodydoc.size().height() + d_bottompadding + d_toppadding + d_verticalmargin + 1);
                          
                          				return size;
                          			}
                          		}
                          	}
                          }
                          
                          #endif // CHATBUBBLEDELEGATE_H
                          
                          

                          And this is my function for writing a message to the chat bubble:

                          void ChatTab::addMessage(QString userColour, QString username, QString message, int itemType, QString itemData)
                          {
                          	QString roleMode;
                          
                          	QColor color(userColour), newColor = color;
                          
                          	int lightnessAmount = 100;
                          
                          	// Convert carriage returns to html carriage returns so they are displayed properly
                          	message.replace("\n", "<br>");
                          
                          	// Crappy way of doing it, but make sure the user colour has a lightness colour of 230 or greater.
                          	// 230 is an arbitrary colour I have chosen
                          	for ( ;; )
                          	{
                          		if ( newColor.lightness() < 230 )
                          		{
                          			lightnessAmount += 10;
                          
                          			newColor = color.lighter(lightnessAmount);
                          		}
                          
                          		else break;
                          	}
                          
                          	// Print the senders name and or time stamp
                          	QString statusText;
                          
                          	if ( m_settings->username == username ) roleMode = "Outgoing";
                          	else roleMode = "Incoming";
                          
                          	if ( m_membersList->count() > 1  && roleMode == "Incoming" ) statusText = username + " ";
                          
                          	statusText += QTime::currentTime().toString( "HH:mm:ss");
                          
                          	struct ChatListItem *cli;
                          
                          	QStandardItem *item1 = new QStandardItem(statusText);
                          
                          	item1->setData(roleMode, Qt::UserRole + 3);
                          
                          	cli = new struct ChatListItem;
                          
                          	cli->itemData = "";
                          	cli->itemText = username;
                          	cli->itemType = CHATLISTITEMTYPE_STATUSMESSAGE;
                          
                          	item1->setData(  QVariant(QVariant::fromValue(static_cast<void*>(cli))), Qt::UserRole + 20 );
                          
                          	m_mymodel.appendRow(item1);
                          
                          	// Print the message
                          	if ( m_settings->username == username ) roleMode = "Outgoing";
                          	else roleMode = "Incoming";
                          
                          	QStandardItem *item2 = new QStandardItem(message);
                          
                          	item2->setData(roleMode, Qt::UserRole + 1);
                          
                          	item2->setData(newColor.name(), Qt::UserRole + 2);
                          
                          	item2->setData("", Qt::UserRole + 3);
                          
                          	cli = new struct ChatListItem;
                          
                          	cli->itemData = itemData;
                          	cli->itemText = message;
                          	cli->itemType = itemType;
                          
                          	item2->setData(  QVariant(QVariant::fromValue(static_cast<void*>(cli))), Qt::UserRole + 20 );
                          
                          	m_mymodel.appendRow(item2);
                          
                          	m_chatList->scrollToBottom();
                          
                          	addToTranscript( username, message );
                          }
                          

                          As I said in my email, it was a while ago when I wrote this. If you have any questions, let me know and I'll try and figure out what I was doing! Lol

                          Small Talk.png

                          This is a screen grab of my chat window. I couldn't grab it with the context menu being displayed, but each bubble has its own context menu which do various things depending on the content of the bubble. Such as downloading an image and displaying it, opening a web page, or copying the bubble text to the clipboard. HTML codes can also be embedded into the bubble and display correctly.
                          I hope all this helps.
                          Steve Q.

                          T Offline
                          T Offline
                          Taytoo
                          wrote on 4 Mar 2024, 17:40 last edited by
                          #12

                          @steveq Thanks so much! Were u able to figure out how to make URLs, sandwiched between other text, clickable? e.g. if message was:

                          Hey, checkout this website: http://forum.qt.io, its really cool!

                          In that only the url should be click, and maybe show hover effect as well. I read some suggestions about using mouse hit-testing to achieve that, but it seems like a lot of work.

                          Another thing I really wanted to do was to make text selectable, but even that seems complicated.

                          1 Reply Last reply
                          0

                          • Login

                          • Login or register to search.
                          • First post
                            Last post
                          0
                          • Categories
                          • Recent
                          • Tags
                          • Popular
                          • Users
                          • Groups
                          • Search
                          • Get Qt Extensions
                          • Unsolved