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