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