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

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.
  • SGaistS Offline
    SGaistS Offline
    SGaist
    Lifetime Qt Champion
    wrote on 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 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
      0
      • B bepaald

        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 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
        0
        • B Offline
          B Offline
          bepaald
          wrote on 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

            @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 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
            5
            • B bepaald

              @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 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
              0
              • S Offline
                S Offline
                steveq
                wrote on 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
                • SGaistS Offline
                  SGaistS Offline
                  SGaist
                  Lifetime Qt Champion
                  wrote on 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
                  1
                  • SGaistS SGaist

                    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 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

                      @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 last edited by steveq
                      #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
                      0
                      • S steveq

                        @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 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