1 | /*************************************************************************** |
2 | * Copyright (C) 2005-2014 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 "tabcompleter.h" |
22 | |
23 | #include "buffermodel.h" |
24 | #include "client.h" |
25 | #include "ircchannel.h" |
26 | #include "ircuser.h" |
27 | #include "multilineedit.h" |
28 | #include "network.h" |
29 | #include "networkmodel.h" |
30 | #include "uisettings.h" |
31 | #include "action.h" |
32 | #include "actioncollection.h" |
33 | #include "graphicalui.h" |
34 | |
35 | #include <QRegExp> |
36 | |
37 | const Network *TabCompleter::_currentNetwork; |
38 | BufferId TabCompleter::_currentBufferId; |
39 | QString TabCompleter::_currentBufferName; |
40 | TabCompleter::Type TabCompleter::_completionType; |
41 | |
42 | TabCompleter::TabCompleter(MultiLineEdit *_lineEdit) |
43 | : QObject(_lineEdit), |
44 | _lineEdit(_lineEdit), |
45 | _enabled(false), |
46 | _nickSuffix(": " ) |
47 | { |
48 | // This Action just serves as a container for the custom shortcut and isn't actually handled; |
49 | // apparently, using tab as an Action shortcut in an input widget is unreliable on some platforms (e.g. OS/2) |
50 | _lineEdit->installEventFilter(this); |
51 | ActionCollection *coll = GraphicalUi::actionCollection("General" ); |
52 | QAction *a = coll->addAction("TabCompletionKey" , new Action(tr("Tab completion" ), coll, |
53 | this, SLOT(onTabCompletionKey()), QKeySequence(Qt::Key_Tab))); |
54 | a->setEnabled(false); // avoid catching the shortcut |
55 | } |
56 | |
57 | |
58 | void TabCompleter::onTabCompletionKey() |
59 | { |
60 | // do nothing; we use the event filter instead |
61 | } |
62 | |
63 | |
64 | void TabCompleter::buildCompletionList() |
65 | { |
66 | // ensure a safe state in case we return early. |
67 | _completionMap.clear(); |
68 | _nextCompletion = _completionMap.begin(); |
69 | |
70 | // this is the first time tab is pressed -> build up the completion list and it's iterator |
71 | QModelIndex currentIndex = Client::bufferModel()->currentIndex(); |
72 | _currentBufferId = currentIndex.data(NetworkModel::BufferIdRole).value<BufferId>(); |
73 | if (!_currentBufferId.isValid()) |
74 | return; |
75 | |
76 | NetworkId networkId = currentIndex.data(NetworkModel::NetworkIdRole).value<NetworkId>(); |
77 | _currentBufferName = currentIndex.sibling(currentIndex.row(), 0).data().toString(); |
78 | |
79 | _currentNetwork = Client::network(networkId); |
80 | if (!_currentNetwork) |
81 | return; |
82 | |
83 | QString tabAbbrev = _lineEdit->text().left(_lineEdit->cursorPosition()).section(QRegExp("[^#\\w\\d-_\\[\\]{}|`^.\\\\]" ), -1, -1); |
84 | QRegExp regex(QString("^[-_\\[\\]{}|`^.\\\\]*" ).append(QRegExp::escape(tabAbbrev)), Qt::CaseInsensitive); |
85 | |
86 | // channel completion - add all channels of the current network to the map |
87 | if (tabAbbrev.startsWith('#')) { |
88 | _completionType = ChannelTab; |
89 | foreach(IrcChannel *ircChannel, _currentNetwork->ircChannels()) { |
90 | if (regex.indexIn(ircChannel->name()) > -1) |
91 | _completionMap[ircChannel->name()] = ircChannel->name(); |
92 | } |
93 | } |
94 | else { |
95 | // user completion |
96 | _completionType = UserTab; |
97 | switch (static_cast<BufferInfo::Type>(currentIndex.data(NetworkModel::BufferTypeRole).toInt())) { |
98 | case BufferInfo::ChannelBuffer: |
99 | { // scope is needed for local var declaration |
100 | IrcChannel *channel = _currentNetwork->ircChannel(_currentBufferName); |
101 | if (!channel) |
102 | return; |
103 | foreach(IrcUser *ircUser, channel->ircUsers()) { |
104 | if (regex.indexIn(ircUser->nick()) > -1) |
105 | _completionMap[ircUser->nick().toLower()] = ircUser->nick(); |
106 | } |
107 | } |
108 | break; |
109 | case BufferInfo::QueryBuffer: |
110 | if (regex.indexIn(_currentBufferName) > -1) |
111 | _completionMap[_currentBufferName.toLower()] = _currentBufferName; |
112 | case BufferInfo::StatusBuffer: |
113 | if (!_currentNetwork->myNick().isEmpty() && regex.indexIn(_currentNetwork->myNick()) > -1) |
114 | _completionMap[_currentNetwork->myNick().toLower()] = _currentNetwork->myNick(); |
115 | break; |
116 | default: |
117 | return; |
118 | } |
119 | } |
120 | |
121 | _nextCompletion = _completionMap.begin(); |
122 | _lastCompletionLength = tabAbbrev.length(); |
123 | } |
124 | |
125 | |
126 | void TabCompleter::complete() |
127 | { |
128 | TabCompletionSettings s; |
129 | _nickSuffix = s.completionSuffix(); |
130 | |
131 | if (!_enabled) { |
132 | buildCompletionList(); |
133 | _enabled = true; |
134 | } |
135 | |
136 | if (_nextCompletion != _completionMap.end()) { |
137 | // clear previous completion |
138 | for (int i = 0; i < _lastCompletionLength; i++) { |
139 | _lineEdit->backspace(); |
140 | } |
141 | |
142 | // insert completion |
143 | _lineEdit->insert(*_nextCompletion); |
144 | |
145 | // remember charcount to delete next time and advance to next completion |
146 | _lastCompletionLength = _nextCompletion->length(); |
147 | _nextCompletion++; |
148 | |
149 | // we're completing the first word of the line |
150 | if (_completionType == UserTab && _lineEdit->cursorPosition() == _lastCompletionLength) { |
151 | _lineEdit->insert(_nickSuffix); |
152 | _lastCompletionLength += _nickSuffix.length(); |
153 | } |
154 | else if (s.addSpaceMidSentence()) { |
155 | _lineEdit->insert(" " ); |
156 | _lastCompletionLength++; |
157 | } |
158 | |
159 | // we're at the end of the list -> start over again |
160 | } |
161 | else { |
162 | if (!_completionMap.isEmpty()) { |
163 | _nextCompletion = _completionMap.begin(); |
164 | complete(); |
165 | } |
166 | } |
167 | } |
168 | |
169 | |
170 | void TabCompleter::reset() |
171 | { |
172 | _enabled = false; |
173 | } |
174 | |
175 | |
176 | bool TabCompleter::eventFilter(QObject *obj, QEvent *event) |
177 | { |
178 | if (obj != _lineEdit || event->type() != QEvent::KeyPress) |
179 | return QObject::eventFilter(obj, event); |
180 | |
181 | QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); |
182 | |
183 | if (keyEvent->key() == GraphicalUi::actionCollection("General" )->action("TabCompletionKey" )->shortcut()[0]) |
184 | complete(); |
185 | else |
186 | reset(); |
187 | |
188 | return false; |
189 | } |
190 | |
191 | |
192 | // this determines the sort order |
193 | bool TabCompleter::CompletionKey::operator<(const CompletionKey &other) const |
194 | { |
195 | switch (_completionType) { |
196 | case UserTab: |
197 | { |
198 | IrcUser *thisUser = _currentNetwork->ircUser(this->contents); |
199 | if (thisUser && _currentNetwork->isMe(thisUser)) |
200 | return false; |
201 | |
202 | IrcUser *thatUser = _currentNetwork->ircUser(other.contents); |
203 | if (thatUser && _currentNetwork->isMe(thatUser)) |
204 | return true; |
205 | |
206 | if (!thisUser || !thatUser) |
207 | return QString::localeAwareCompare(this->contents, other.contents) < 0; |
208 | |
209 | QDateTime thisSpokenTo = thisUser->lastSpokenTo(_currentBufferId); |
210 | QDateTime thatSpokenTo = thatUser->lastSpokenTo(_currentBufferId); |
211 | |
212 | if (thisSpokenTo.isValid() || thatSpokenTo.isValid()) |
213 | return thisSpokenTo > thatSpokenTo; |
214 | |
215 | QDateTime thisTime = thisUser->lastChannelActivity(_currentBufferId); |
216 | QDateTime thatTime = thatUser->lastChannelActivity(_currentBufferId); |
217 | |
218 | if (thisTime.isValid() || thatTime.isValid()) |
219 | return thisTime > thatTime; |
220 | } |
221 | break; |
222 | case ChannelTab: |
223 | if (QString::compare(_currentBufferName, this->contents, Qt::CaseInsensitive) == 0) |
224 | return true; |
225 | |
226 | if (QString::compare(_currentBufferName, other.contents, Qt::CaseInsensitive) == 0) |
227 | return false; |
228 | break; |
229 | default: |
230 | break; |
231 | } |
232 | |
233 | return QString::localeAwareCompare(this->contents, other.contents) < 0; |
234 | } |
235 | |