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