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