Add configuration option for a custom stylesheet
[quassel.git] / src / qtui / chatlinemodelitem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-09 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  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, 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
41 unsigned char *ChatLineModelItem::TextBoundaryFinderBuffer = (unsigned char *)malloc(512 * sizeof(HB_CharAttributes_Dummy));
42 int ChatLineModelItem::TextBoundaryFinderBufferSize = 512 * (sizeof(HB_CharAttributes_Dummy) / sizeof(unsigned char));
43
44 // ****************************************
45 // the actual ChatLineModelItem
46 // ****************************************
47 ChatLineModelItem::ChatLineModelItem(const Message &msg)
48   : MessageModelItem(),
49     _styledMsg(msg)
50 {
51   if(!msg.sender().contains('!'))
52     _styledMsg.setFlags(msg.flags() |= Message::ServerMsg);
53 }
54
55 QVariant ChatLineModelItem::data(int column, int role) const {
56   if(role == ChatLineModel::MsgLabelRole)
57     return messageLabel();
58
59   QVariant variant;
60   MessageModel::ColumnType col = (MessageModel::ColumnType)column;
61   switch(col) {
62   case ChatLineModel::TimestampColumn:
63     variant = timestampData(role);
64     break;
65   case ChatLineModel::SenderColumn:
66     variant = senderData(role);
67     break;
68   case ChatLineModel::ContentsColumn:
69     variant = contentsData(role);
70     break;
71   default:
72     break;
73   }
74   if(!variant.isValid())
75     return MessageModelItem::data(column, role);
76   return variant;
77 }
78
79 QVariant ChatLineModelItem::timestampData(int role) const {
80   switch(role) {
81   case ChatLineModel::DisplayRole:
82     return _styledMsg.decoratedTimestamp();
83   case ChatLineModel::EditRole:
84     return _styledMsg.timestamp();
85   case ChatLineModel::BackgroundRole:
86     return backgroundBrush(UiStyle::Timestamp);
87   case ChatLineModel::SelectedBackgroundRole:
88     return backgroundBrush(UiStyle::Timestamp, true);
89   case ChatLineModel::FormatRole:
90     return QVariant::fromValue<UiStyle::FormatList>(UiStyle::FormatList()
91                       << qMakePair((quint16)0, (quint32)UiStyle::formatType(_styledMsg.type()) | UiStyle::Timestamp));
92   }
93   return QVariant();
94 }
95
96 QVariant ChatLineModelItem::senderData(int role) const {
97   switch(role) {
98   case ChatLineModel::DisplayRole:
99     return _styledMsg.decoratedSender();
100   case ChatLineModel::EditRole:
101     return _styledMsg.plainSender();
102   case ChatLineModel::BackgroundRole:
103     return backgroundBrush(UiStyle::Sender);
104   case ChatLineModel::SelectedBackgroundRole:
105     return backgroundBrush(UiStyle::Sender, true);
106   case ChatLineModel::FormatRole:
107     return QVariant::fromValue<UiStyle::FormatList>(UiStyle::FormatList()
108                       << qMakePair((quint16)0, (quint32)UiStyle::formatType(_styledMsg.type()) | UiStyle::Sender));
109   }
110   return QVariant();
111 }
112
113 QVariant ChatLineModelItem::contentsData(int role) const {
114   switch(role) {
115   case ChatLineModel::DisplayRole:
116   case ChatLineModel::EditRole:
117     return _styledMsg.plainContents();
118   case ChatLineModel::BackgroundRole:
119     return backgroundBrush(UiStyle::Contents);
120   case ChatLineModel::SelectedBackgroundRole:
121     return backgroundBrush(UiStyle::Contents, true);
122   case ChatLineModel::FormatRole:
123     return QVariant::fromValue<UiStyle::FormatList>(_styledMsg.contentsFormatList());
124   case ChatLineModel::WrapListRole:
125     if(_wrapList.isEmpty())
126       computeWrapList();
127     return QVariant::fromValue<ChatLineModel::WrapList>(_wrapList);
128   }
129   return QVariant();
130 }
131
132 quint32 ChatLineModelItem::messageLabel() const {
133   quint32 label = _styledMsg.senderHash() << 16;
134   if(_styledMsg.flags() & Message::Self)
135     label |= UiStyle::OwnMsg;
136   if(_styledMsg.flags() & Message::Highlight)
137     label |= UiStyle::Highlight;
138   return label;
139 }
140
141 QVariant ChatLineModelItem::backgroundBrush(UiStyle::FormatType subelement, bool selected) const {
142   QTextCharFormat fmt = QtUi::style()->format(UiStyle::formatType(_styledMsg.type()) | subelement, messageLabel() | (selected ? UiStyle::Selected : 0));
143   if(fmt.hasProperty(QTextFormat::BackgroundBrush))
144     return QVariant::fromValue<QBrush>(fmt.background());
145   return QVariant();
146 }
147
148 void ChatLineModelItem::computeWrapList() const {
149   int length = _styledMsg.plainContents().length();
150   if(!length)
151     return;
152
153   enum Mode { SearchStart, SearchEnd };
154
155   QList<ChatLineModel::Word> wplist;  // use a temp list which we'll later copy into a QVector for efficiency
156   QTextBoundaryFinder finder(QTextBoundaryFinder::Word, _styledMsg.plainContents().unicode(), length,
157                               TextBoundaryFinderBuffer, TextBoundaryFinderBufferSize);
158
159   int idx;
160   int oldidx = 0;
161   bool wordStart = false;
162   bool wordEnd = false;
163   Mode mode = SearchEnd;
164   ChatLineModel::Word word;
165   word.start = 0;
166   qreal wordstartx = 0;
167
168   QTextLayout layout(_styledMsg.plainContents());
169   QTextOption option;
170   option.setWrapMode(QTextOption::NoWrap);
171   layout.setTextOption(option);
172
173   layout.setAdditionalFormats(QtUi::style()->toTextLayoutList(_styledMsg.contentsFormatList(), length, messageLabel()));
174   layout.beginLayout();
175   QTextLine line = layout.createLine();
176   line.setNumColumns(length);
177   layout.endLayout();
178
179   do {
180     idx = finder.toNextBoundary();
181     if(idx < 0) {
182       idx = length;
183       wordStart = false;
184       wordEnd = false;
185       mode = SearchStart;
186     } else {
187       wordStart = finder.boundaryReasons().testFlag(QTextBoundaryFinder::StartWord);
188       wordEnd = finder.boundaryReasons().testFlag(QTextBoundaryFinder::EndWord);
189     }
190
191     //if(flg) qDebug() << idx << mode << wordStart << wordEnd << contents->plainText.left(idx) << contents->plainText.mid(idx);
192
193     if(mode == SearchEnd || (!wordStart && wordEnd)) {
194       if(wordStart || !wordEnd) continue;
195       oldidx = idx;
196       mode = SearchStart;
197       continue;
198     }
199     qreal wordendx = line.cursorToX(oldidx);
200     qreal trailingendx = line.cursorToX(idx);
201     word.endX = wordendx;
202     word.width = wordendx - wordstartx;
203     word.trailing = trailingendx - wordendx;
204     wordstartx = trailingendx;
205     wplist.append(word);
206
207     if(wordStart) {
208       word.start = idx;
209       mode = SearchEnd;
210     }
211     // the part " || (finder.position() == contents->plainText.length())" shouldn't be necessary
212     // but in rare and indeterministic cases Qt states that the end of the text is not a boundary o_O
213   } while(finder.isAtBoundary() || (finder.position() == length));
214
215   // A QVector needs less space than a QList
216   _wrapList.resize(wplist.count());
217   for(int i = 0; i < wplist.count(); i++) {
218     _wrapList[i] = wplist.at(i);
219   }
220 }
221