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