added support for RichText input and conversion to mirc format
[quassel.git] / src / qtui / inputwidget.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 "inputwidget.h"
22
23 #include "action.h"
24 #include "actioncollection.h"
25 #include "bufferview.h"
26 #include "client.h"
27 #include "iconloader.h"
28 #include "ircuser.h"
29 #include "jumpkeyhandler.h"
30 #include "networkmodel.h"
31 #include "qtui.h"
32 #include "qtuisettings.h"
33 #include "tabcompleter.h"
34
35 InputWidget::InputWidget(QWidget *parent)
36   : AbstractItemView(parent),
37     _networkId(0)
38 {
39   ui.setupUi(this);
40   connect(ui.ownNick, SIGNAL(activated(QString)), this, SLOT(changeNick(QString)));
41
42   layout()->setAlignment(ui.ownNick, Qt::AlignBottom);
43   layout()->setAlignment(ui.inputEdit, Qt::AlignBottom);
44
45   setFocusProxy(ui.inputEdit);
46   ui.ownNick->setFocusProxy(ui.inputEdit);
47
48   ui.ownNick->setSizeAdjustPolicy(QComboBox::AdjustToContents);
49   ui.ownNick->installEventFilter(new MouseWheelFilter(this));
50   ui.inputEdit->installEventFilter(new JumpKeyHandler(this));
51   ui.inputEdit->installEventFilter(this);
52
53   ui.inputEdit->setMinHeight(1);
54   ui.inputEdit->setMaxHeight(5);
55   ui.inputEdit->setMode(MultiLineEdit::MultiLine);
56   ui.inputEdit->setPasteProtectionEnabled(true);
57
58   new TabCompleter(ui.inputEdit);
59
60   UiStyleSettings fs("Fonts");
61   fs.notify("UseCustomInputWidgetFont", this, SLOT(setUseCustomFont(QVariant)));
62   fs.notify("InputWidget", this, SLOT(setCustomFont(QVariant)));
63   if(fs.value("UseCustomInputWidgetFont", false).toBool())
64     setCustomFont(fs.value("InputWidget", QFont()));
65
66   UiSettings s("InputWidget");
67
68 #ifdef HAVE_KDE
69   s.notify("EnableSpellCheck", this, SLOT(setEnableSpellCheck(QVariant)));
70   setEnableSpellCheck(s.value("EnableSpellCheck", false));
71 #endif
72
73   s.notify("ShowNickSelector", this, SLOT(setShowNickSelector(QVariant)));
74   setShowNickSelector(s.value("ShowNickSelector", true));
75
76   s.notify("MaxNumLines", this, SLOT(setMaxLines(QVariant)));
77   setMaxLines(s.value("MaxNumLines", 5));
78
79   s.notify("EnableScrollBars", this, SLOT(setScrollBarsEnabled(QVariant)));
80   setScrollBarsEnabled(s.value("EnableScrollBars", true));
81
82   s.notify("EnableMultiLine", this, SLOT(setMultiLineEnabled(QVariant)));
83   setMultiLineEnabled(s.value("EnableMultiLine", true));
84
85   ActionCollection *coll = QtUi::actionCollection();
86
87   Action *activateInputline = coll->add<Action>("FocusInputLine");
88   connect(activateInputline, SIGNAL(triggered()), SLOT(setFocus()));
89   activateInputline->setText(tr("Focus Input Line"));
90   activateInputline->setShortcut(tr("Ctrl+L"));
91
92   actionTextBold = coll->add<Action>("TextBold");
93   QFont bold;
94   bold.setBold(true);
95   actionTextBold->setFont(bold);
96   actionTextBold->setText(tr("Bold"));
97   actionTextBold->setShortcut(tr("Ctrl+B"));
98   actionTextBold->setCheckable(true);
99   connect(actionTextBold, SIGNAL(triggered()), SLOT(textBold()));
100
101   actionTextUnderline = coll->add<Action>("TextUnderline");
102   QFont underline;
103   underline.setUnderline(true);
104   actionTextUnderline->setFont(underline);
105   actionTextUnderline->setText(tr("Underline"));
106   actionTextUnderline->setShortcut(tr("Ctrl+U"));
107   actionTextUnderline->setCheckable(true);
108   connect(actionTextUnderline, SIGNAL(triggered()), SLOT(textUnderline()));
109
110   actionTextItalic = coll->add<Action>("TextItalic");
111   QFont italic;
112   italic.setItalic(true);
113   actionTextItalic->setFont(italic);
114   actionTextItalic->setText(tr("Italic"));
115   actionTextItalic->setShortcut(tr("Ctrl+I"));
116   actionTextItalic->setCheckable(true);
117   connect(actionTextItalic, SIGNAL(triggered()), SLOT(textItalic()));
118
119   connect(inputLine(), SIGNAL(currentCharFormatChanged(QTextCharFormat)), this, SLOT(currentCharFormatChanged(QTextCharFormat)));
120 }
121
122 InputWidget::~InputWidget() {
123 }
124
125 void InputWidget::setUseCustomFont(const QVariant &v) {
126   if(v.toBool()) {
127     UiStyleSettings fs("Fonts");
128     setCustomFont(fs.value("InputWidget"));
129   } else
130     setCustomFont(QFont());
131 }
132
133 void InputWidget::setCustomFont(const QVariant &v) {
134   QFont font = v.value<QFont>();
135   if(font.family().isEmpty())
136     font = QApplication::font();
137   ui.inputEdit->setCustomFont(font);
138 }
139
140 void InputWidget::setEnableSpellCheck(const QVariant &v) {
141   ui.inputEdit->setSpellCheckEnabled(v.toBool());
142 }
143
144 void InputWidget::setShowNickSelector(const QVariant &v) {
145   ui.ownNick->setVisible(v.toBool());
146 }
147
148 void InputWidget::setMaxLines(const QVariant &v) {
149   ui.inputEdit->setMaxHeight(v.toInt());
150 }
151
152 void InputWidget::setScrollBarsEnabled(const QVariant &v) {
153   ui.inputEdit->setScrollBarsEnabled(v.toBool());
154 }
155
156 void InputWidget::setMultiLineEnabled(const QVariant &v) {
157   ui.inputEdit->setMode(v.toBool()? MultiLineEdit::MultiLine : MultiLineEdit::SingleLine);
158 }
159
160 bool InputWidget::eventFilter(QObject *watched, QEvent *event) {
161   if(event->type() != QEvent::KeyPress)
162     return false;
163
164   QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
165
166   // keys from BufferView should be sent to (and focus) the input line
167   BufferView *view = qobject_cast<BufferView *>(watched);
168   if(view) {
169     if(keyEvent->text().length() == 1 && !(keyEvent->modifiers() & (Qt::ControlModifier ^ Qt::AltModifier)) ) { // normal key press
170       QChar c = keyEvent->text().at(0);
171       if(c.isLetterOrNumber() || c.isSpace() || c.isPunct() || c.isSymbol()) {
172         setFocus();
173         QCoreApplication::sendEvent(inputLine(), keyEvent);
174         return true;
175       }
176     }
177     return false;
178   } else if(watched == ui.inputEdit) {
179     if(keyEvent->matches(QKeySequence::Find)) {
180       QAction *act = GraphicalUi::actionCollection()->action("ToggleSearchBar");
181       if(act) {
182         act->toggle();
183         return true;
184       }
185     }
186     return false;
187   }
188   return false;
189 }
190
191 void InputWidget::currentChanged(const QModelIndex &current, const QModelIndex &previous) {
192   Q_UNUSED(previous)
193   NetworkId networkId = current.data(NetworkModel::NetworkIdRole).value<NetworkId>();
194   if(networkId == _networkId)
195     return;
196
197   setNetwork(networkId);
198   updateNickSelector();
199   updateEnabledState();
200 }
201
202 void InputWidget::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) {
203   QItemSelectionRange changedArea(topLeft, bottomRight);
204   if(changedArea.contains(selectionModel()->currentIndex())) {
205     updateEnabledState();
206   }
207 };
208
209 void InputWidget::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) {
210   NetworkId networkId;
211   QModelIndex child;
212   for(int row = start; row <= end; row++) {
213     child = model()->index(row, 0, parent);
214     if(NetworkModel::NetworkItemType != child.data(NetworkModel::ItemTypeRole).toInt())
215       continue;
216     networkId = child.data(NetworkModel::NetworkIdRole).value<NetworkId>();
217     if(networkId == _networkId) {
218       setNetwork(0);
219       updateNickSelector();
220       return;
221     }
222   }
223 }
224
225 void InputWidget::updateEnabledState() {
226   QModelIndex currentIndex = selectionModel()->currentIndex();
227
228   const Network *net = Client::networkModel()->networkByIndex(currentIndex);
229   bool enabled = false;
230   if(net) {
231     // disable inputline if it's a channelbuffer we parted from or...
232     enabled = (currentIndex.data(NetworkModel::ItemActiveRole).value<bool>() || (currentIndex.data(NetworkModel::BufferTypeRole).toInt() != BufferInfo::ChannelBuffer));
233     // ... if we're not connected to the network at all
234     enabled &= net->isConnected();
235   }
236   ui.inputEdit->setEnabled(enabled);
237 }
238
239 const Network *InputWidget::currentNetwork() const {
240   return Client::network(_networkId);
241 }
242
243 BufferInfo InputWidget::currentBufferInfo() const {
244   return selectionModel()->currentIndex().data(NetworkModel::BufferInfoRole).value<BufferInfo>();
245 };
246
247 void InputWidget::setNetwork(NetworkId networkId) {
248   if(_networkId == networkId)
249     return;
250
251   const Network *previousNet = Client::network(_networkId);
252   if(previousNet) {
253     disconnect(previousNet, 0, this, 0);
254     if(previousNet->me())
255       disconnect(previousNet->me(), 0, this, 0);
256   }
257
258   _networkId = networkId;
259
260   const Network *network = Client::network(networkId);
261   if(network) {
262     connect(network, SIGNAL(identitySet(IdentityId)), this, SLOT(setIdentity(IdentityId)));
263     connectMyIrcUser();
264     setIdentity(network->identity());
265   } else {
266     setIdentity(0);
267     _networkId = 0;
268   }
269 }
270
271 void InputWidget::connectMyIrcUser() {
272   const Network *network = currentNetwork();
273   if(network->me()) {
274     connect(network->me(), SIGNAL(nickSet(const QString &)), this, SLOT(updateNickSelector()));
275     connect(network->me(), SIGNAL(userModesSet(QString)), this, SLOT(updateNickSelector()));
276     connect(network->me(), SIGNAL(userModesAdded(QString)), this, SLOT(updateNickSelector()));
277     connect(network->me(), SIGNAL(userModesRemoved(QString)), this, SLOT(updateNickSelector()));
278     connect(network->me(), SIGNAL(awaySet(bool)), this, SLOT(updateNickSelector()));
279     disconnect(network, SIGNAL(myNickSet(const QString &)), this, SLOT(connectMyIrcUser()));
280     updateNickSelector();
281   } else {
282     connect(network, SIGNAL(myNickSet(const QString &)), this, SLOT(connectMyIrcUser()));
283   }
284 }
285
286 void InputWidget::setIdentity(IdentityId identityId) {
287   if(_identityId == identityId)
288     return;
289
290   const Identity *previousIdentity = Client::identity(_identityId);
291   if(previousIdentity)
292     disconnect(previousIdentity, 0, this, 0);
293
294   _identityId = identityId;
295
296   const Identity *identity = Client::identity(identityId);
297   if(identity) {
298     connect(identity, SIGNAL(nicksSet(QStringList)), this, SLOT(updateNickSelector()));
299   } else {
300     _identityId = 0;
301   }
302   updateNickSelector();
303 }
304
305 void InputWidget::updateNickSelector() const {
306   ui.ownNick->clear();
307
308   const Network *net = currentNetwork();
309   if(!net)
310     return;
311
312   const Identity *identity = Client::identity(net->identity());
313   if(!identity) {
314     qWarning() << "InputWidget::updateNickSelector(): can't find Identity for Network" << net->networkId() << "IdentityId:" << net->identity();
315     return;
316   }
317
318   int nickIdx;
319   QStringList nicks = identity->nicks();
320   if((nickIdx = nicks.indexOf(net->myNick())) == -1) {
321     nicks.prepend(net->myNick());
322     nickIdx = 0;
323   }
324
325   if(nicks.isEmpty())
326     return;
327
328   IrcUser *me = net->me();
329   if(me) {
330     nicks[nickIdx] = net->myNick();
331     if(!me->userModes().isEmpty())
332       nicks[nickIdx] += QString(" (+%1)").arg(me->userModes());
333   }
334
335   ui.ownNick->addItems(nicks);
336
337   if(me && me->isAway())
338     ui.ownNick->setItemData(nickIdx, SmallIcon("user-away"), Qt::DecorationRole);
339
340   ui.ownNick->setCurrentIndex(nickIdx);
341 }
342
343 void InputWidget::changeNick(const QString &newNick) const {
344   const Network *net = currentNetwork();
345   if(!net || net->isMyNick(newNick))
346     return;
347
348   // we reset the nick selecter as we have no confirmation yet, that this will succeed.
349   // if the action succeeds it will be properly updated anyways.
350   updateNickSelector();
351   Client::userInput(BufferInfo::fakeStatusBuffer(net->networkId()), QString("/NICK %1").arg(newNick));
352 }
353
354 void InputWidget::on_inputEdit_textEntered(const QString &text) const {
355   Client::userInput(currentBufferInfo(), text);
356   actionTextBold->setChecked(false);
357   actionTextUnderline->setChecked(false);
358   actionTextItalic->setChecked(false);
359   inputLine()->setFontWeight(QFont::Normal);
360   inputLine()->setFontUnderline(false);
361   inputLine()->setFontItalic(false);
362 }
363
364 void InputWidget::mergeFormatOnWordOrSelection(const QTextCharFormat &format) {
365   QTextCursor cursor = inputLine()->textCursor();
366   if (!cursor.hasSelection())
367     cursor.select(QTextCursor::WordUnderCursor);
368   cursor.mergeCharFormat(format);
369   inputLine()->mergeCurrentCharFormat(format);
370 }
371
372 void InputWidget::currentCharFormatChanged(const QTextCharFormat &format) {
373   fontChanged(format.font());
374   colorChanged(format.foreground().color(), format.background().color());
375 }
376
377 void InputWidget::textBold()
378 {
379   if (inputLine()->hasFocus()) {
380     QTextCharFormat fmt;
381     fmt.setFontWeight(actionTextBold->isChecked() ? QFont::Bold : QFont::Normal);
382     mergeFormatOnWordOrSelection(fmt);
383   }
384 }
385
386 void InputWidget::textUnderline()
387 {
388   if (inputLine()->hasFocus()) {
389     QTextCharFormat fmt;
390     fmt.setFontUnderline(actionTextUnderline->isChecked());
391     mergeFormatOnWordOrSelection(fmt);
392   }
393 }
394
395 void InputWidget::textItalic()
396 {
397   if (inputLine()->hasFocus()) {
398     QTextCharFormat fmt;
399     fmt.setFontItalic(actionTextItalic->isChecked());
400     mergeFormatOnWordOrSelection(fmt);
401   }
402 }
403
404 void InputWidget::fontChanged(const QFont &f)
405 {
406   actionTextBold->setChecked(f.bold());
407   actionTextItalic->setChecked(f.italic());
408   actionTextUnderline->setChecked(f.underline());
409 }
410
411 void InputWidget::colorChanged(const QColor &fg, const QColor &bg) {
412   //TODO update colorpicker
413 }
414
415 // MOUSE WHEEL FILTER
416 MouseWheelFilter::MouseWheelFilter(QObject *parent)
417   : QObject(parent)
418 {
419 }
420
421 bool MouseWheelFilter::eventFilter(QObject *obj, QEvent *event) {
422   if(event->type() != QEvent::Wheel)
423     return QObject::eventFilter(obj, event);
424   else
425     return true;
426 }