1 /***************************************************************************
2 * Copyright (C) 2005-2019 by the Quassel Project *
3 * devel@quassel-irc.org *
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. *
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. *
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 ***************************************************************************/
21 #include "contextmenuactionprovider.h"
23 #include <QInputDialog>
26 #include <QMessageBox>
28 #include "buffermodel.h"
29 #include "buffersettings.h"
31 #include "clientignorelistmanager.h"
36 ContextMenuActionProvider::ContextMenuActionProvider(QObject* parent)
37 : NetworkModelController(parent)
39 registerAction(NetworkConnect, icon::get("network-connect"), tr("Connect"));
40 registerAction(NetworkDisconnect, icon::get("network-disconnect"), tr("Disconnect"));
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"));
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..."));
58 registerAction(JoinChannel, icon::get("irc-join-channel"), tr("Join Channel..."));
60 registerAction(NickQuery, tr("Start Query"));
61 registerAction(NickSwitchTo, tr("Show Query"));
62 registerAction(NickWhois, tr("Whois"));
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..."));
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);
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"));
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"));
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);
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);
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);
135 auto* ignoreMenu = new QMenu();
136 _nickIgnoreMenuAction = new Action(tr("Ignore"), nullptr);
137 _nickIgnoreMenuAction->setMenu(ignoreMenu);
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);
147 ContextMenuActionProvider::~ContextMenuActionProvider()
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();
161 void ContextMenuActionProvider::addActions(QMenu* menu, BufferId bufId, ActionSlot slot)
163 if (!bufId.isValid())
165 addActions(menu, Client::networkModel()->bufferIndex(bufId), std::move(slot));
168 void ContextMenuActionProvider::addActions(QMenu* menu, const QModelIndex& index, ActionSlot slot, bool isCustomBufferView)
170 if (!index.isValid())
172 addActions(menu, QList<QModelIndex>() << index, nullptr, QString(), std::move(slot), isCustomBufferView);
175 void ContextMenuActionProvider::addActions(QMenu* menu, MessageFilter* filter, BufferId msgBuffer, ActionSlot slot)
177 addActions(menu, filter, msgBuffer, QString(), std::move(slot));
180 void ContextMenuActionProvider::addActions(QMenu* menu, MessageFilter* filter, BufferId msgBuffer, const QString& chanOrNick, ActionSlot slot)
184 addActions(menu, QList<QModelIndex>() << Client::networkModel()->bufferIndex(msgBuffer), filter, chanOrNick, std::move(slot), false);
187 void ContextMenuActionProvider::addActions(QMenu* menu, const QList<QModelIndex>& indexList, ActionSlot slot, bool isCustomBufferView)
189 addActions(menu, indexList, nullptr, QString(), std::move(slot), isCustomBufferView);
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)
200 if (!indexList_.count())
203 setIndexList(indexList_);
204 setMessageFilter(filter_);
205 setContextItem(contextItem_);
206 setSlot(std::move(actionSlot));
208 if (!messageFilter()) {
209 // this means we are in a BufferView (or NickView) rather than a ChatView
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());
216 case NetworkModel::NetworkItemType:
217 addNetworkItemActions(menu, index);
219 case NetworkModel::BufferItemType:
220 addBufferItemActions(menu, index, isCustomBufferView);
222 case NetworkModel::IrcUserItemType:
223 addIrcUserActions(menu, index);
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));
239 addBufferItemActions(menu, index);
243 // TODO: actions for merged buffers... _indexList contains the index of the message we clicked on
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())
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);
261 addAction(JoinChannel, menu);
264 // TODO: actions for a nick
270 void ContextMenuActionProvider::addNetworkItemActions(QMenu* menu, const QModelIndex& index)
272 NetworkId networkId = index.data(NetworkModel::NetworkIdRole).value<NetworkId>();
273 if (!networkId.isValid())
275 const Network* network = Client::network(networkId);
276 Q_CHECK_PTR(network);
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);
289 void ContextMenuActionProvider::addBufferItemActions(QMenu* menu, const QModelIndex& index, bool isCustomBufferView)
291 BufferInfo bufferInfo = index.data(NetworkModel::BufferInfoRole).value<BufferInfo>();
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);
306 case BufferInfo::QueryBuffer: {
307 // IrcUser *ircUser = qobject_cast<IrcUser *>(index.data(NetworkModel::IrcUserRole).value<QObject *>());
309 addIrcUserActions(menu, index);
310 menu->addSeparator();
312 addHideEventsMenu(menu, bufferInfo.bufferId());
313 menu->addSeparator();
314 addAction(HideBufferTemporarily, menu, isCustomBufferView);
315 addAction(HideBufferPermanently, menu, isCustomBufferView);
316 addAction(BufferRemove, menu, index);
321 addAction(HideBufferTemporarily, menu, isCustomBufferView);
322 addAction(HideBufferPermanently, menu, isCustomBufferView);
326 void ContextMenuActionProvider::addIrcUserActions(QMenu* menu, const QModelIndex& index)
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)
333 if (contextItem().isNull()) {
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);
340 auto* ircUser = qobject_cast<IrcUser*>(index.data(NetworkModel::IrcUserRole).value<QObject*>());
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);
349 action(NickHalfop)->setVisible(false);
350 action(NickDehalfop)->setVisible(false);
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(),
360 addIgnoreMenu(menu, ircUser->hostmask(), ignoreMap);
361 // end of ignoreliststuff
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);
369 else if (!contextItem().isEmpty() && messageFilter()) {
375 Action* ContextMenuActionProvider::addAction(ActionType type, QMenu* menu, const QModelIndex& index, ItemActiveStates requiredActiveState)
377 return addAction(action(type), menu, checkRequirements(index, requiredActiveState));
380 Action* ContextMenuActionProvider::addAction(Action* action, QMenu* menu, const QModelIndex& index, ItemActiveStates requiredActiveState)
382 return addAction(action, menu, checkRequirements(index, requiredActiveState));
385 Action* ContextMenuActionProvider::addAction(ActionType type, QMenu* menu, bool condition)
387 return addAction(action(type), menu, condition);
390 Action* ContextMenuActionProvider::addAction(Action* action, QMenu* menu, bool condition)
393 menu->addAction(action);
394 action->setVisible(true);
397 action->setVisible(false);
402 void ContextMenuActionProvider::addHideEventsMenu(QMenu* menu, BufferId bufferId)
404 if (BufferSettings(bufferId).hasFilter())
405 addHideEventsMenu(menu, BufferSettings(bufferId).messageFilter());
407 addHideEventsMenu(menu);
410 void ContextMenuActionProvider::addHideEventsMenu(QMenu* menu, MessageFilter* msgFilter)
412 if (BufferSettings(msgFilter->idString()).hasFilter())
413 addHideEventsMenu(menu, BufferSettings(msgFilter->idString()).messageFilter());
415 addHideEventsMenu(menu);
418 void ContextMenuActionProvider::addHideEventsMenu(QMenu* menu, int filter)
420 action(HideApplyToAll)->setEnabled(filter != -1);
421 action(HideUseDefaults)->setEnabled(filter != -1);
423 filter = BufferSettings().messageFilter();
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);
433 menu->addAction(_hideEventsMenuAction);
436 void ContextMenuActionProvider::addIgnoreMenu(QMenu* menu, const QString& hostmask, const QMap<QString, bool>& ignoreMap)
438 QMenu* ignoreMenu = _nickIgnoreMenuAction->menu();
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();
451 // add "Add Ignore Rule" description
452 ignoreMenu->addAction(_ignoreDescriptions.at(0));
456 text = QString("*!%1@%2").arg(ident, host);
457 action(NickIgnoreUser)->setText(text);
458 action(NickIgnoreUser)->setProperty("ignoreRule", text);
460 text = QString("*!*@%1").arg(host);
461 action(NickIgnoreHost)->setText(text);
462 action(NickIgnoreHost)->setProperty("ignoreRule", text);
464 text = domain.at(0) == '.' ? QString("*!%1@*%2").arg(ident, domain) : QString("*!%1@%2").arg(ident, domain);
466 action(NickIgnoreDomain)->setText(text);
467 action(NickIgnoreDomain)->setProperty("ignoreRule", text);
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));
480 action(NickIgnoreCustom)->setProperty("ignoreRule", hostmask);
481 ignoreMenu->addAction(action(NickIgnoreCustom));
483 ignoreMenu->addSeparator();
486 QMap<QString, bool>::const_iterator ruleIter = ignoreMap.begin();
488 if (!ignoreMap.isEmpty())
489 // add "Existing Rules" description
490 ignoreMenu->addAction(_ignoreDescriptions.at(1));
491 while (ruleIter != ignoreMap.constEnd()) {
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);
504 ignoreMenu->addSeparator();
506 ignoreMenu->addAction(action(ShowIgnoreList));
507 addAction(_nickIgnoreMenuAction, menu);