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 <QInputDialog>
22#include <QMenu>
23#include <QMessageBox>
24#include <QMap>
25
26#include "contextmenuactionprovider.h"
27
28#include "buffermodel.h"
29#include "buffersettings.h"
30#include "iconloader.h"
31#include "clientidentity.h"
32#include "network.h"
33#include "util.h"
34#include "client.h"
35#include "clientignorelistmanager.h"
36
37ContextMenuActionProvider::ContextMenuActionProvider(QObject *parent) : NetworkModelController(parent)
38{
39 registerAction(NetworkConnect, SmallIcon("network-connect"), tr("Connect"));
40 registerAction(NetworkDisconnect, SmallIcon("network-disconnect"), tr("Disconnect"));
41
42 registerAction(BufferJoin, SmallIcon("irc-join-channel"), tr("Join"));
43 registerAction(BufferPart, SmallIcon("irc-close-channel"), tr("Part"));
44 registerAction(BufferRemove, tr("Delete Chat(s)..."));
45 registerAction(BufferSwitchTo, tr("Go to Chat"));
46
47 registerAction(HideJoin, tr("Joins"), true);
48 registerAction(HidePart, tr("Parts"), true);
49 registerAction(HideQuit, tr("Quits"), true);
50 registerAction(HideNick, tr("Nick Changes"), true);
51 registerAction(HideMode, tr("Mode Changes"), true);
52 registerAction(HideDayChange, tr("Day Changes"), true);
53 registerAction(HideTopic, tr("Topic Changes"), true);
54 registerAction(HideApplyToAll, tr("Set as Default..."));
55 registerAction(HideUseDefaults, tr("Use Defaults..."));
56
57 registerAction(JoinChannel, SmallIcon("irc-join-channel"), tr("Join Channel..."));
58
59 registerAction(NickQuery, tr("Start Query"));
60 registerAction(NickSwitchTo, tr("Show Query"));
61 registerAction(NickWhois, tr("Whois"));
62
63 registerAction(NickCtcpVersion, tr("Version"));
64 registerAction(NickCtcpTime, tr("Time"));
65 registerAction(NickCtcpPing, tr("Ping"));
66 registerAction(NickCtcpClientinfo, tr("Client info"));
67 registerAction(NickIgnoreCustom, tr("Custom..."));
68
69 // these texts are only dummies! don't think about tr() here!
70 registerAction(NickIgnoreUser, "*!ident@host.domain.tld");
71 registerAction(NickIgnoreHost, "*!*@host.domain.tld");
72 registerAction(NickIgnoreDomain, "*!ident@*.domain.tld");
73 registerAction(NickIgnoreToggleEnabled0, "Enable", true);
74 registerAction(NickIgnoreToggleEnabled1, "Enable", true);
75 registerAction(NickIgnoreToggleEnabled2, "Enable", true);
76 registerAction(NickIgnoreToggleEnabled3, "Enable", true);
77 registerAction(NickIgnoreToggleEnabled4, "Enable", true);
78
79 registerAction(NickOp, SmallIcon("irc-operator"), tr("Give Operator Status"));
80 registerAction(NickDeop, SmallIcon("irc-remove-operator"), tr("Take Operator Status"));
81 registerAction(NickHalfop, SmallIcon("irc-voice"), tr("Give Half-Operator Status"));
82 registerAction(NickDehalfop, SmallIcon("irc-unvoice"), tr("Take Half-Operator Status"));
83 registerAction(NickVoice, SmallIcon("irc-voice"), tr("Give Voice"));
84 registerAction(NickDevoice, SmallIcon("irc-unvoice"), tr("Take Voice"));
85 registerAction(NickKick, SmallIcon("im-kick-user"), tr("Kick From Channel"));
86 registerAction(NickBan, SmallIcon("im-ban-user"), tr("Ban From Channel"));
87 registerAction(NickKickBan, SmallIcon("im-ban-kick-user"), tr("Kick && Ban"));
88
89 registerAction(HideBufferTemporarily, tr("Hide Chat(s) Temporarily"));
90 registerAction(HideBufferPermanently, tr("Hide Chat(s) Permanently"));
91 registerAction(ShowChannelList, tr("Show Channel List"));
92 registerAction(ShowIgnoreList, tr("Show Ignore List"));
93
94 QMenu *hideEventsMenu = new QMenu();
95 hideEventsMenu->addAction(action(HideJoin));
96 hideEventsMenu->addAction(action(HidePart));
97 hideEventsMenu->addAction(action(HideQuit));
98 hideEventsMenu->addAction(action(HideNick));
99 hideEventsMenu->addAction(action(HideMode));
100 hideEventsMenu->addAction(action(HideTopic));
101 hideEventsMenu->addAction(action(HideDayChange));
102 hideEventsMenu->addSeparator();
103 hideEventsMenu->addAction(action(HideApplyToAll));
104 hideEventsMenu->addAction(action(HideUseDefaults));
105 _hideEventsMenuAction = new Action(tr("Hide Events"), 0);
106 _hideEventsMenuAction->setMenu(hideEventsMenu);
107
108 QMenu *nickCtcpMenu = new QMenu();
109 nickCtcpMenu->addAction(action(NickCtcpPing));
110 nickCtcpMenu->addAction(action(NickCtcpVersion));
111 nickCtcpMenu->addAction(action(NickCtcpTime));
112 nickCtcpMenu->addAction(action(NickCtcpClientinfo));
113 _nickCtcpMenuAction = new Action(tr("CTCP"), 0);
114 _nickCtcpMenuAction->setMenu(nickCtcpMenu);
115
116 QMenu *nickModeMenu = new QMenu();
117 nickModeMenu->addAction(action(NickOp));
118 nickModeMenu->addAction(action(NickDeop));
119 // this is where the halfops will be placed if available
120 nickModeMenu->addAction(action(NickHalfop));
121 nickModeMenu->addAction(action(NickDehalfop));
122 nickModeMenu->addAction(action(NickVoice));
123 nickModeMenu->addAction(action(NickDevoice));
124 nickModeMenu->addSeparator();
125 nickModeMenu->addAction(action(NickKick));
126 nickModeMenu->addAction(action(NickBan));
127 nickModeMenu->addAction(action(NickKickBan));
128 _nickModeMenuAction = new Action(tr("Actions"), 0);
129 _nickModeMenuAction->setMenu(nickModeMenu);
130
131 QMenu *ignoreMenu = new QMenu();
132 _nickIgnoreMenuAction = new Action(tr("Ignore"), 0);
133 _nickIgnoreMenuAction->setMenu(ignoreMenu);
134
135 // These are disabled actions used as descriptions
136 // They don't need any of the Action fancyness so we use plain QActions
137 _ignoreDescriptions << new QAction(tr("Add Ignore Rule"), this);
138 _ignoreDescriptions << new QAction(tr("Existing Rules"), this);
139 foreach(QAction *act, _ignoreDescriptions)
140 act->setEnabled(false);
141}
142
143
144ContextMenuActionProvider::~ContextMenuActionProvider()
145{
146 _hideEventsMenuAction->menu()->deleteLater();
147 _hideEventsMenuAction->deleteLater();
148 _nickCtcpMenuAction->menu()->deleteLater();
149 _nickCtcpMenuAction->deleteLater();
150 _nickModeMenuAction->menu()->deleteLater();
151 _nickModeMenuAction->deleteLater();
152 _nickIgnoreMenuAction->menu()->deleteLater();
153 _nickIgnoreMenuAction->deleteLater();
154 qDeleteAll(_ignoreDescriptions);
155 _ignoreDescriptions.clear();
156}
157
158
159void ContextMenuActionProvider::addActions(QMenu *menu, BufferId bufId, QObject *receiver, const char *method)
160{
161 if (!bufId.isValid())
162 return;
163 addActions(menu, Client::networkModel()->bufferIndex(bufId), receiver, method);
164}
165
166
167void ContextMenuActionProvider::addActions(QMenu *menu, const QModelIndex &index, QObject *receiver, const char *method, bool isCustomBufferView)
168{
169 if (!index.isValid())
170 return;
171 addActions(menu, QList<QModelIndex>() << index, 0, QString(), receiver, method, isCustomBufferView);
172}
173
174
175void ContextMenuActionProvider::addActions(QMenu *menu, MessageFilter *filter, BufferId msgBuffer, QObject *receiver, const char *slot)
176{
177 addActions(menu, filter, msgBuffer, QString(), receiver, slot);
178}
179
180
181void ContextMenuActionProvider::addActions(QMenu *menu, MessageFilter *filter, BufferId msgBuffer, const QString &chanOrNick, QObject *receiver, const char *method)
182{
183 if (!filter)
184 return;
185 addActions(menu, QList<QModelIndex>() << Client::networkModel()->bufferIndex(msgBuffer), filter, chanOrNick, receiver, method, false);
186}
187
188
189void ContextMenuActionProvider::addActions(QMenu *menu, const QList<QModelIndex> &indexList, QObject *receiver, const char *method, bool isCustomBufferView)
190{
191 addActions(menu, indexList, 0, QString(), receiver, method, isCustomBufferView);
192}
193
194
195// add a list of actions sensible for the current item(s)
196void ContextMenuActionProvider::addActions(QMenu *menu,
197 const QList<QModelIndex> &indexList_,
198 MessageFilter *filter_,
199 const QString &contextItem_,
200 QObject *receiver_,
201 const char *method_,
202 bool isCustomBufferView)
203{
204 if (!indexList_.count())
205 return;
206
207 setIndexList(indexList_);
208 setMessageFilter(filter_);
209 setContextItem(contextItem_);
210 setSlot(receiver_, method_);
211
212 if (!messageFilter()) {
213 // this means we are in a BufferView (or NickView) rather than a ChatView
214
215 // first index in list determines the menu type (just in case we have both buffers and networks selected, for example)
216 QModelIndex index = indexList().at(0);
217 NetworkModel::ItemType itemType = static_cast<NetworkModel::ItemType>(index.data(NetworkModel::ItemTypeRole).toInt());
218
219 switch (itemType) {
220 case NetworkModel::NetworkItemType:
221 addNetworkItemActions(menu, index);
222 break;
223 case NetworkModel::BufferItemType:
224 addBufferItemActions(menu, index, isCustomBufferView);
225 break;
226 case NetworkModel::IrcUserItemType:
227 addIrcUserActions(menu, index);
228 break;
229 default:
230 return;
231 }
232 }
233 else {
234 // ChatView actions
235 if (contextItem().isEmpty()) {
236 // a) query buffer: handle like ircuser
237 // b) general chatview: handle like channel iff it displays a single buffer
238 // NOTE stuff breaks probably with merged buffers, need to rework a lot around here then
239 if (messageFilter()->containedBuffers().count() == 1) {
240 // we can handle this like a single bufferItem
241 QModelIndex index = Client::networkModel()->bufferIndex(messageFilter()->containedBuffers().values().at(0));
242 setIndexList(index);
243 addBufferItemActions(menu, index);
244 return;
245 }
246 else {
247 // TODO: actions for merged buffers... _indexList contains the index of the message we clicked on
248 }
249 }
250 else {
251 // context item = chan or nick, _indexList = buf where the msg clicked on originated
252 if (isChannelName(contextItem())) {
253 QModelIndex msgIdx = indexList().at(0);
254 if (!msgIdx.isValid())
255 return;
256 NetworkId networkId = msgIdx.data(NetworkModel::NetworkIdRole).value<NetworkId>();
257 BufferId bufId = Client::networkModel()->bufferId(networkId, contextItem());
258 if (bufId.isValid()) {
259 QModelIndex targetIdx = Client::networkModel()->bufferIndex(bufId);
260 setIndexList(targetIdx);
261 addAction(BufferJoin, menu, targetIdx, InactiveState);
262 addAction(BufferSwitchTo, menu, targetIdx, ActiveState);
263 }
264 else
265 addAction(JoinChannel, menu);
266 }
267 else {
268 // TODO: actions for a nick
269 }
270 }
271 }
272}
273
274
275void ContextMenuActionProvider::addNetworkItemActions(QMenu *menu, const QModelIndex &index)
276{
277 NetworkId networkId = index.data(NetworkModel::NetworkIdRole).value<NetworkId>();
278 if (!networkId.isValid())
279 return;
280 const Network *network = Client::network(networkId);
281 Q_CHECK_PTR(network);
282 if (!network)
283 return;
284
285 addAction(NetworkConnect, menu, network->connectionState() == Network::Disconnected);
286 addAction(NetworkDisconnect, menu, network->connectionState() != Network::Disconnected);
287 menu->addSeparator();
288 addAction(ShowChannelList, menu, index, ActiveState);
289 addAction(JoinChannel, menu, index, ActiveState);
290}
291
292
293void ContextMenuActionProvider::addBufferItemActions(QMenu *menu, const QModelIndex &index, bool isCustomBufferView)
294{
295 BufferInfo bufferInfo = index.data(NetworkModel::BufferInfoRole).value<BufferInfo>();
296
297 menu->addSeparator();
298 switch (bufferInfo.type()) {
299 case BufferInfo::ChannelBuffer:
300 addAction(BufferJoin, menu, index, InactiveState);
301 addAction(BufferPart, menu, index, ActiveState);
302 menu->addSeparator();
303 addHideEventsMenu(menu, bufferInfo.bufferId());
304 menu->addSeparator();
305 addAction(HideBufferTemporarily, menu, isCustomBufferView);
306 addAction(HideBufferPermanently, menu, isCustomBufferView);
307 addAction(BufferRemove, menu, index, InactiveState);
308 break;
309
310 case BufferInfo::QueryBuffer:
311 {
312 //IrcUser *ircUser = qobject_cast<IrcUser *>(index.data(NetworkModel::IrcUserRole).value<QObject *>());
313 //if(ircUser) {
314 addIrcUserActions(menu, index);
315 menu->addSeparator();
316 //}
317 addHideEventsMenu(menu, bufferInfo.bufferId());
318 menu->addSeparator();
319 addAction(HideBufferTemporarily, menu, isCustomBufferView);
320 addAction(HideBufferPermanently, menu, isCustomBufferView);
321 addAction(BufferRemove, menu, index);
322 break;
323 }
324
325 default:
326 addAction(HideBufferTemporarily, menu, isCustomBufferView);
327 addAction(HideBufferPermanently, menu, isCustomBufferView);
328 }
329}
330
331
332void ContextMenuActionProvider::addIrcUserActions(QMenu *menu, const QModelIndex &index)
333{
334 // this can be called: a) as a nicklist context menu (index has IrcUserItemType)
335 // b) as a query buffer context menu (index has BufferItemType and is a QueryBufferItem)
336 // c) right-click in a query chatview (same as b), index will be the corresponding QueryBufferItem)
337 // d) right-click on some nickname (_contextItem will be non-null, _filter -> chatview, index -> message buffer)
338
339 if (contextItem().isNull()) {
340 // cases a, b, c
341 bool haveQuery = indexList().count() == 1 && findQueryBuffer(index).isValid();
342 NetworkModel::ItemType itemType = static_cast<NetworkModel::ItemType>(index.data(NetworkModel::ItemTypeRole).toInt());
343 addAction(_nickModeMenuAction, menu, itemType == NetworkModel::IrcUserItemType);
344 addAction(_nickCtcpMenuAction, menu);
345
346 IrcUser *ircUser = qobject_cast<IrcUser *>(index.data(NetworkModel::IrcUserRole).value<QObject *>());
347 if (ircUser) {
348 Network *network = ircUser->network();
349 // only show entries for usermode +h if server supports it
350 if (network && network->prefixModes().contains('h')) {
351 action(NickHalfop)->setVisible(true);
352 action(NickDehalfop)->setVisible(true);
353 }
354 else {
355 action(NickHalfop)->setVisible(false);
356 action(NickDehalfop)->setVisible(false);
357 }
358 // ignoreliststuff
359 QString bufferName;
360 BufferInfo bufferInfo = index.data(NetworkModel::BufferInfoRole).value<BufferInfo>();
361 if (bufferInfo.type() == BufferInfo::ChannelBuffer)
362 bufferName = bufferInfo.bufferName();
363 QMap<QString, bool> ignoreMap = Client::ignoreListManager()->matchingRulesForHostmask(ircUser->hostmask(), ircUser->network()->networkName(), bufferName);
364 addIgnoreMenu(menu, ircUser->hostmask(), ignoreMap);
365 // end of ignoreliststuff
366 }
367 menu->addSeparator();
368 addAction(NickQuery, menu, itemType == NetworkModel::IrcUserItemType && !haveQuery && indexList().count() == 1);
369 addAction(NickSwitchTo, menu, itemType == NetworkModel::IrcUserItemType && haveQuery);
370 menu->addSeparator();
371 addAction(NickWhois, menu, true);
372 }
373 else if (!contextItem().isEmpty() && messageFilter()) {
374 // case d
375 // TODO
376 }
377}
378
379
380Action *ContextMenuActionProvider::addAction(ActionType type, QMenu *menu, const QModelIndex &index, ItemActiveStates requiredActiveState)
381{
382 return addAction(action(type), menu, checkRequirements(index, requiredActiveState));
383}
384
385
386Action *ContextMenuActionProvider::addAction(Action *action, QMenu *menu, const QModelIndex &index, ItemActiveStates requiredActiveState)
387{
388 return addAction(action, menu, checkRequirements(index, requiredActiveState));
389}
390
391
392Action *ContextMenuActionProvider::addAction(ActionType type, QMenu *menu, bool condition)
393{
394 return addAction(action(type), menu, condition);
395}
396
397
398Action *ContextMenuActionProvider::addAction(Action *action, QMenu *menu, bool condition)
399{
400 if (condition) {
401 menu->addAction(action);
402 action->setVisible(true);
403 }
404 else {
405 action->setVisible(false);
406 }
407 return action;
408}
409
410
411void ContextMenuActionProvider::addHideEventsMenu(QMenu *menu, BufferId bufferId)
412{
413 if (BufferSettings(bufferId).hasFilter())
414 addHideEventsMenu(menu, BufferSettings(bufferId).messageFilter());
415 else
416 addHideEventsMenu(menu);
417}
418
419
420void ContextMenuActionProvider::addHideEventsMenu(QMenu *menu, MessageFilter *msgFilter)
421{
422 if (BufferSettings(msgFilter->idString()).hasFilter())
423 addHideEventsMenu(menu, BufferSettings(msgFilter->idString()).messageFilter());
424 else
425 addHideEventsMenu(menu);
426}
427
428
429void ContextMenuActionProvider::addHideEventsMenu(QMenu *menu, int filter)
430{
431 action(HideApplyToAll)->setEnabled(filter != -1);
432 action(HideUseDefaults)->setEnabled(filter != -1);
433 if (filter == -1)
434 filter = BufferSettings().messageFilter();
435
436 action(HideJoin)->setChecked(filter & Message::Join);
437 action(HidePart)->setChecked(filter & Message::Part);
438 action(HideQuit)->setChecked(filter & Message::Quit);
439 action(HideNick)->setChecked(filter & Message::Nick);
440 action(HideMode)->setChecked(filter & Message::Mode);
441 action(HideDayChange)->setChecked(filter & Message::DayChange);
442 action(HideTopic)->setChecked(filter & Message::Topic);
443
444 menu->addAction(_hideEventsMenuAction);
445}
446
447
448void ContextMenuActionProvider::addIgnoreMenu(QMenu *menu, const QString &hostmask, const QMap<QString, bool> &ignoreMap)
449{
450 QMenu *ignoreMenu = _nickIgnoreMenuAction->menu();
451 ignoreMenu->clear();
452 QString nick = nickFromMask(hostmask);
453 QString ident = userFromMask(hostmask);
454 QString host = hostFromMask(hostmask);
455 QString domain = host;
456 QRegExp domainRx = QRegExp("(\\.[^.]+\\.\\w+\\D)$");
457 if (domainRx.indexIn(host) != -1)
458 domain = domainRx.cap(1);
459 // we can't rely on who-data
460 // if we don't have the data, we skip actions where we would need it
461 bool haveWhoData = !ident.isEmpty() && !host.isEmpty();
462
463 // add "Add Ignore Rule" description
464 ignoreMenu->addAction(_ignoreDescriptions.at(0));
465
466 if (haveWhoData) {
467 QString text;
468 text = QString("*!%1@%2").arg(ident, host);
469 action(NickIgnoreUser)->setText(text);
470 action(NickIgnoreUser)->setProperty("ignoreRule", text);
471
472 text = QString("*!*@%1").arg(host);
473 action(NickIgnoreHost)->setText(text);
474 action(NickIgnoreHost)->setProperty("ignoreRule", text);
475
476 text = domain.at(0) == '.' ? QString("*!%1@*%2").arg(ident, domain)
477 : QString("*!%1@%2").arg(ident, domain);
478
479 action(NickIgnoreDomain)->setText(text);
480 action(NickIgnoreDomain)->setProperty("ignoreRule", text);
481
482 if (!ignoreMap.contains(action(NickIgnoreUser)->property("ignoreRule").toString()))
483 ignoreMenu->addAction(action(NickIgnoreUser));
484 if (!ignoreMap.contains(action(NickIgnoreHost)->property("ignoreRule").toString()))
485 ignoreMenu->addAction(action(NickIgnoreHost));
486 // we only add that NickIgnoreDomain if it isn't the same as NickIgnoreUser
487 // as happens with @foobar.com hostmasks and ips
488 if (!ignoreMap.contains(action(NickIgnoreDomain)->property("ignoreRule").toString())
489 && action(NickIgnoreUser)->property("ignoreRule").toString() != action(NickIgnoreDomain)->property("ignoreRule").toString())
490 ignoreMenu->addAction(action(NickIgnoreDomain));
491 }
492
493 action(NickIgnoreCustom)->setProperty("ignoreRule", hostmask);
494 ignoreMenu->addAction(action(NickIgnoreCustom));
495
496 ignoreMenu->addSeparator();
497
498 if (haveWhoData) {
499 QMap<QString, bool>::const_iterator ruleIter = ignoreMap.begin();
500 int counter = 0;
501 if (!ignoreMap.isEmpty())
502 // add "Existing Rules" description
503 ignoreMenu->addAction(_ignoreDescriptions.at(1));
504 while (ruleIter != ignoreMap.constEnd()) {
505 if (counter < 5) {
506 ActionType type = static_cast<ActionType>(NickIgnoreToggleEnabled0 + counter*0x100000);
507 Action *act = action(type);
508 act->setText(ruleIter.key());
509 act->setProperty("ignoreRule", ruleIter.key());
510 act->setChecked(ruleIter.value());
511 ignoreMenu->addAction(act);
512 }
513 counter++;
514 ruleIter++;
515 }
516 if (counter)
517 ignoreMenu->addSeparator();
518 }
519 ignoreMenu->addAction(action(ShowIgnoreList));
520 addAction(_nickIgnoreMenuAction, menu);
521}
522