Make URLs stylable and fix on-hover underline
[quassel.git] / src / qtui / chatitem.h
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 #ifndef CHATITEM_H_
22 #define CHATITEM_H_
23
24 #include <QAction>
25 #include <QGraphicsItem>
26 #include <QObject>
27
28 #include "chatlinemodel.h"
29 #include "chatscene.h"
30 #include "uistyle.h"
31 #include "qtui.h"
32
33 #include <QTextLayout>
34
35 class ChatItem : public QGraphicsItem {
36 protected:
37   ChatItem(const qreal &width, const qreal &height, const QPointF &pos, QGraphicsItem *parent);
38
39 public:
40   inline const QAbstractItemModel *model() const;
41   inline int row() const;
42   virtual ChatLineModel::ColumnType column() const = 0;
43   inline ChatScene *chatScene() const { return qobject_cast<ChatScene *>(scene()); }
44
45   inline QRectF boundingRect() const { return _boundingRect; }
46   inline qreal width() const { return _boundingRect.width(); }
47   inline qreal height() const { return _boundingRect.height(); }
48
49   void initLayoutHelper(QTextLayout *layout, QTextOption::WrapMode, Qt::Alignment = Qt::AlignLeft) const;
50   virtual inline void initLayout(QTextLayout *layout) const {
51     initLayoutHelper(layout, QTextOption::NoWrap);
52     doLayout(layout);
53   }
54   virtual void doLayout(QTextLayout *) const;
55   virtual UiStyle::FormatList formatList() const;
56
57   virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0);
58   enum { Type = ChatScene::ChatItemType };
59   virtual inline int type() const { return Type; }
60
61   QVariant data(int role) const;
62
63   // selection stuff, to be called by the scene
64   QString selection() const;
65   void clearSelection();
66   void setFullSelection();
67   void continueSelecting(const QPointF &pos);
68   bool hasSelection() const;
69   bool isPosOverSelection(const QPointF &pos) const;
70
71   QList<QRectF> findWords(const QString &searchWord, Qt::CaseSensitivity caseSensitive);
72
73   virtual void addActionsToMenu(QMenu *menu, const QPointF &itemPos);
74   virtual void handleClick(const QPointF &pos, ChatScene::ClickMode);
75
76 protected:
77   enum SelectionMode {
78     NoSelection,
79     PartialSelection,
80     FullSelection
81   };
82
83   virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
84   virtual void mousePressEvent(QGraphicsSceneMouseEvent *event);
85   virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
86
87   void paintBackground(QPainter *);
88   QVector<QTextLayout::FormatRange> selectionFormats() const;
89   virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
90   void overlayFormat(UiStyle::FormatList &fmtList, int start, int end, quint32 overlayFmt) const;
91
92   inline qint16 selectionStart() const { return _selectionStart; }
93   inline void setSelectionStart(qint16 start) { _selectionStart = start; }
94   inline qint16 selectionEnd() const { return _selectionEnd; }
95   inline void setSelectionEnd(qint16 end) { _selectionEnd = end; }
96   inline SelectionMode selectionMode() const { return _selectionMode; }
97   inline void setSelectionMode(SelectionMode mode) { _selectionMode = mode; }
98   void setSelection(SelectionMode mode, qint16 selectionStart, qint16 selectionEnd);
99
100   qint16 posToCursor(const QPointF &pos) const;
101
102   // WARNING: setGeometry and setHeight should not be used without either:
103   //  a) calling prepareGeometryChange() immediately before setColumns()
104   //  b) calling Chatline::setPos() immediately afterwards
105   inline void setGeometry(qreal width, qreal height) {
106     _boundingRect.setWidth(width);
107     _boundingRect.setHeight(height);
108   }
109   inline void setHeight(const qreal &height) {
110     _boundingRect.setHeight(height);
111   }
112   inline void setWidth(const qreal &width) {
113     _boundingRect.setWidth(width);
114   }
115
116 private:
117   // internal selection stuff
118   void setSelection(int start, int length);
119
120   QRectF _boundingRect;
121
122   SelectionMode _selectionMode;
123   qint16 _selectionStart, _selectionEnd;
124
125   friend class ChatLine;
126 };
127
128 // ************************************************************
129 // TimestampChatItem
130 // ************************************************************
131
132 //! A ChatItem for the timestamp column
133 class TimestampChatItem : public ChatItem {
134 public:
135   TimestampChatItem(const qreal &width, const qreal &height, QGraphicsItem *parent) : ChatItem(width, height, QPointF(0, 0), parent) {}
136   enum { Type = ChatScene::TimestampChatItemType };
137   virtual inline int type() const { return Type; }
138   virtual inline ChatLineModel::ColumnType column() const { return ChatLineModel::TimestampColumn; }
139 };
140
141 // ************************************************************
142 // SenderChatItem
143 // ************************************************************
144 //! A ChatItem for the sender column
145 class SenderChatItem : public ChatItem {
146 public:
147   SenderChatItem(const qreal &width, const qreal &height, const QPointF &pos, QGraphicsItem *parent) : ChatItem(width, height, pos, parent) {}
148   virtual inline ChatLineModel::ColumnType column() const { return ChatLineModel::SenderColumn; }
149
150 protected:
151   virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0);
152   enum { Type = ChatScene::SenderChatItemType };
153   virtual inline int type() const { return Type; }
154   virtual inline void initLayout(QTextLayout *layout) const {
155     initLayoutHelper(layout, QTextOption::ManualWrap, Qt::AlignRight);
156     doLayout(layout);
157   }
158 };
159
160 // ************************************************************
161 // ContentsChatItem
162 // ************************************************************
163 struct ContentsChatItemPrivate;
164
165 //! A ChatItem for the contents column
166 class ContentsChatItem : public ChatItem {
167   Q_DECLARE_TR_FUNCTIONS(ContentsChatItem);
168
169 public:
170   ContentsChatItem(const qreal &width, const QPointF &pos, QGraphicsItem *parent);
171   ~ContentsChatItem();
172
173   enum { Type = ChatScene::ContentsChatItemType };
174   virtual inline int type() const { return Type; }
175
176   inline ChatLineModel::ColumnType column() const { return ChatLineModel::ContentsColumn; }
177   QFontMetricsF *fontMetrics() const;
178
179 protected:
180   virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
181   virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event);
182   virtual void hoverMoveEvent(QGraphicsSceneHoverEvent *event);
183   virtual void handleClick(const QPointF &pos, ChatScene::ClickMode clickMode);
184
185   virtual void addActionsToMenu(QMenu *menu, const QPointF &itemPos);
186   virtual void copyLinkToClipboard();
187
188   virtual QVector<QTextLayout::FormatRange> additionalFormats() const;
189
190   virtual inline void initLayout(QTextLayout *layout) const {
191     initLayoutHelper(layout, QTextOption::WrapAnywhere);
192     doLayout(layout);
193   }
194   virtual void doLayout(QTextLayout *layout) const;
195   virtual UiStyle::FormatList formatList() const;
196
197 private:
198   struct Clickable;
199   class ActionProxy;
200   class WrapColumnFinder;
201
202   ContentsChatItemPrivate *_data;
203   ContentsChatItemPrivate *privateData() const;
204
205   QList<Clickable> findClickables() const;
206   Clickable clickableAt(const QPointF &pos) const;
207
208   void endHoverMode();
209   void showWebPreview(const Clickable &click);
210   void clearWebPreview();
211
212   qreal setGeometryByWidth(qreal w);
213   friend class ChatLine;
214   friend struct ContentsChatItemPrivate;
215
216   QFontMetricsF *_fontMetrics;
217
218   // we need a receiver for Action signals
219   static ActionProxy _actionProxy;
220 };
221
222 struct ContentsChatItem::Clickable {
223   // Don't change these enums without also changing the regexps in analyze()!
224   enum Type {
225     Invalid = -1,
226     Url = 0,
227     Channel = 1,
228     Nick = 2
229   };
230
231   Type type;
232   quint16 start;
233   quint16 length;
234
235   inline Clickable() : type(Invalid) {};
236   inline Clickable(Type type_, quint16 start_, quint16 length_) : type(type_), start(start_), length(length_) {};
237   inline bool isValid() const { return type != Invalid; }
238 };
239
240 struct ContentsChatItemPrivate {
241   ContentsChatItem *contentsItem;
242   QList<ContentsChatItem::Clickable> clickables;
243   ContentsChatItem::Clickable currentClickable;
244   ContentsChatItem::Clickable activeClickable;
245
246   ContentsChatItemPrivate(const QList<ContentsChatItem::Clickable> &c, ContentsChatItem *parent) : contentsItem(parent), clickables(c) {}
247 };
248
249 class ContentsChatItem::WrapColumnFinder {
250 public:
251   WrapColumnFinder(const ChatItem *parent);
252   ~WrapColumnFinder();
253
254   qint16 nextWrapColumn(qreal width);
255
256 private:
257   const ChatItem *item;
258   QTextLayout layout;
259   QTextLine line;
260   ChatLineModel::WrapList wrapList;
261   qint16 wordidx;
262   qint16 lineCount;
263   qreal choppedTrailing;
264 };
265
266 //! Acts as a proxy for Action signals targetted at a ContentsChatItem
267 /** Since a ChatItem is not a QObject, hence cannot receive signals, we use a static ActionProxy
268  *  as a receiver instead. This avoids having to handle ChatItem actions (e.g. context menu entries)
269  *  outside the ChatItem.
270  */
271 class ContentsChatItem::ActionProxy : public QObject {
272   Q_OBJECT
273
274 public slots:
275   inline void copyLinkToClipboard() { item()->copyLinkToClipboard(); }
276
277 private:
278   /// Returns the ContentsChatItem that should receive the action event.
279   /** For efficiency reasons, values are not checked for validity. You gotta make sure that you set the data() member
280    *  in the Action correctly.
281    *  @return The ChatItem from which the sending Action originated
282    */
283   inline ContentsChatItem *item() const {
284     return static_cast<ContentsChatItem *>(qobject_cast<QAction *>(sender())->data().value<void *>());
285   }
286 };
287
288 /*************************************************************************************************/
289
290 // Avoid circular include deps
291 #include "chatline.h"
292 const QAbstractItemModel *ChatItem::model() const { return static_cast<ChatLine *>(parentItem())->model(); }
293 int ChatItem::row() const { return static_cast<ChatLine *>(parentItem())->row(); }
294
295 #endif