Merge pull request #97 from Bombe/focus-host-input
[quassel.git] / src / qtui / chatlinemodelitem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2015 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include <QFontMetrics>
22 #include <QTextBoundaryFinder>
23
24 #include "chatlinemodelitem.h"
25 #include "chatlinemodel.h"
26 #include "qtui.h"
27 #include "qtuistyle.h"
28
29 // This Struct is taken from Harfbuzz. We use it only to calc it's size.
30 // we use a shared memory region so we do not have to malloc a buffer area for every line
31 typedef struct {
32     /*HB_LineBreakType*/ unsigned lineBreakType  : 2;
33     /*HB_Bool*/ unsigned whiteSpace              : 1;     /* A unicode whitespace character, except NBSP, ZWNBSP */
34     /*HB_Bool*/ unsigned charStop                : 1;     /* Valid cursor position (for left/right arrow) */
35     /*HB_Bool*/ unsigned wordBoundary            : 1;
36     /*HB_Bool*/ unsigned sentenceBoundary        : 1;
37     unsigned unused                  : 2;
38 } HB_CharAttributes_Dummy;
39
40 unsigned char *ChatLineModelItem::TextBoundaryFinderBuffer = (unsigned char *)malloc(512 * sizeof(HB_CharAttributes_Dummy));
41 int ChatLineModelItem::TextBoundaryFinderBufferSize = 512 * (sizeof(HB_CharAttributes_Dummy) / sizeof(unsigned char));
42
43 // ****************************************
44 // the actual ChatLineModelItem
45 // ****************************************
46 ChatLineModelItem::ChatLineModelItem(const Message &msg)
47     : MessageModelItem(),
48     _styledMsg(msg)
49 {
50     if (!msg.sender().contains('!'))
51         _styledMsg.setFlags(msg.flags() |= Message::ServerMsg);
52 }
53
54
55 bool ChatLineModelItem::setData(int column, const QVariant &value, int role)
56 {
57     switch (role) {
58     case MessageModel::FlagsRole:
59         _styledMsg.setFlags((Message::Flags)value.toUInt());
60         return true;
61     default:
62         return MessageModelItem::setData(column, value, role);
63     }
64 }
65
66
67 QVariant ChatLineModelItem::data(int column, int role) const
68 {
69     if (role == ChatLineModel::MsgLabelRole)
70         return messageLabel();
71
72     QVariant variant;
73     MessageModel::ColumnType col = (MessageModel::ColumnType)column;
74     switch (col) {
75     case ChatLineModel::TimestampColumn:
76         variant = timestampData(role);
77         break;
78     case ChatLineModel::SenderColumn:
79         variant = senderData(role);
80         break;
81     case ChatLineModel::ContentsColumn:
82         variant = contentsData(role);
83         break;
84     default:
85         break;
86     }
87     if (!variant.isValid())
88         return MessageModelItem::data(column, role);
89     return variant;
90 }
91
92
93 QVariant ChatLineModelItem::timestampData(int role) const
94 {
95     switch (role) {
96     case ChatLineModel::DisplayRole:
97         return _styledMsg.decoratedTimestamp();
98     case ChatLineModel::EditRole:
99         return _styledMsg.timestamp();
100     case ChatLineModel::BackgroundRole:
101         return backgroundBrush(UiStyle::Timestamp);
102     case ChatLineModel::SelectedBackgroundRole:
103         return backgroundBrush(UiStyle::Timestamp, true);
104     case ChatLineModel::FormatRole:
105         return QVariant::fromValue<UiStyle::FormatList>(UiStyle::FormatList()
106                                                         << qMakePair((quint16)0, (quint32) UiStyle::formatType(_styledMsg.type()) | UiStyle::Timestamp));
107     }
108     return QVariant();
109 }
110
111
112 QVariant ChatLineModelItem::senderData(int role) const
113 {
114     switch (role) {
115     case ChatLineModel::DisplayRole:
116         return _styledMsg.decoratedSender();
117     case ChatLineModel::EditRole:
118         return _styledMsg.plainSender();
119     case ChatLineModel::BackgroundRole:
120         return backgroundBrush(UiStyle::Sender);
121     case ChatLineModel::SelectedBackgroundRole:
122         return backgroundBrush(UiStyle::Sender, true);
123     case ChatLineModel::FormatRole:
124         return QVariant::fromValue<UiStyle::FormatList>(UiStyle::FormatList()
125                                                         << qMakePair((quint16)0, (quint32) UiStyle::formatType(_styledMsg.type()) | UiStyle::Sender));
126     }
127     return QVariant();
128 }
129
130
131 QVariant ChatLineModelItem::contentsData(int role) const
132 {
133     switch (role) {
134     case ChatLineModel::DisplayRole:
135     case ChatLineModel::EditRole:
136         return _styledMsg.plainContents();
137     case ChatLineModel::BackgroundRole:
138         return backgroundBrush(UiStyle::Contents);
139     case ChatLineModel::SelectedBackgroundRole:
140         return backgroundBrush(UiStyle::Contents, true);
141     case ChatLineModel::FormatRole:
142         return QVariant::fromValue<UiStyle::FormatList>(_styledMsg.contentsFormatList());
143     case ChatLineModel::WrapListRole:
144         if (_wrapList.isEmpty())
145             computeWrapList();
146         return QVariant::fromValue<ChatLineModel::WrapList>(_wrapList);
147     }
148     return QVariant();
149 }
150
151
152 quint32 ChatLineModelItem::messageLabel() const
153 {
154     quint32 label = _styledMsg.senderHash() << 16;
155     if (_styledMsg.flags() & Message::Self)
156         label |= UiStyle::OwnMsg;
157     if (_styledMsg.flags() & Message::Highlight)
158         label |= UiStyle::Highlight;
159     return label;
160 }
161
162
163 QVariant ChatLineModelItem::backgroundBrush(UiStyle::FormatType subelement, bool selected) const
164 {
165     QTextCharFormat fmt = QtUi::style()->format(UiStyle::formatType(_styledMsg.type()) | subelement, messageLabel() | (selected ? UiStyle::Selected : 0));
166     if (fmt.hasProperty(QTextFormat::BackgroundBrush))
167         return QVariant::fromValue<QBrush>(fmt.background());
168     return QVariant();
169 }
170
171
172 void ChatLineModelItem::computeWrapList() const
173 {
174     QString text = _styledMsg.plainContents();
175     int length = text.length();
176     if (!length)
177         return;
178
179     QList<ChatLineModel::Word> wplist; // use a temp list which we'll later copy into a QVector for efficiency
180     QTextBoundaryFinder finder(QTextBoundaryFinder::Line, _styledMsg.plainContents().unicode(), length,
181         TextBoundaryFinderBuffer, TextBoundaryFinderBufferSize);
182
183     int idx;
184     int oldidx = 0;
185     ChatLineModel::Word word;
186     word.start = 0;
187     qreal wordstartx = 0;
188
189     QTextLayout layout(_styledMsg.plainContents());
190     QTextOption option;
191     option.setWrapMode(QTextOption::NoWrap);
192     layout.setTextOption(option);
193
194     layout.setAdditionalFormats(QtUi::style()->toTextLayoutList(_styledMsg.contentsFormatList(), length, messageLabel()));
195     layout.beginLayout();
196     QTextLine line = layout.createLine();
197     line.setNumColumns(length);
198     layout.endLayout();
199
200     while ((idx = finder.toNextBoundary()) >= 0 && idx <= length) {
201         // QTextBoundaryFinder has inconsistent behavior in Qt version up to and including 4.6.3 (at least).
202         // It doesn't point to the position we should break, but to the character before that.
203         // Unfortunately Qt decided to fix this by changing the behavior of QTBF, so now we have to add a version
204         // check. At the time of this writing, I'm still trying to get this reverted upstream...
205         //
206         // cf. https://bugs.webkit.org/show_bug.cgi?id=31076 and Qt commit e6ac173
207         static int needWorkaround = -1;
208         if (needWorkaround < 0) {
209             needWorkaround = 0;
210             QStringList versions = QString(qVersion()).split('.');
211             if (versions.count() == 3 && versions.at(0).toInt() == 4) {
212                 if (versions.at(1).toInt() <= 6 && versions.at(2).toInt() <= 3)
213                     needWorkaround = 1;
214             }
215         }
216         if (needWorkaround == 1) {
217             if (idx < length)
218                 idx++;
219         }
220
221         if (idx == oldidx)
222             continue;
223
224         word.start = oldidx;
225         int wordend = idx;
226         for (; wordend > word.start; wordend--) {
227             if (!text.at(wordend-1).isSpace())
228                 break;
229         }
230
231         qreal wordendx = line.cursorToX(wordend);
232         qreal trailingendx = line.cursorToX(idx);
233         word.endX = wordendx;
234         word.width = wordendx - wordstartx;
235         word.trailing = trailingendx - wordendx;
236         wordstartx = trailingendx;
237         wplist.append(word);
238
239         oldidx = idx;
240     }
241
242     // A QVector needs less space than a QList
243     _wrapList.resize(wplist.count());
244     for (int i = 0; i < wplist.count(); i++) {
245         _wrapList[i] = wplist.at(i);
246     }
247 }