1 | /*************************************************************************** |
2 | * Copyright (C) 2005-2014 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 | } |
248 | |