/***************************************************************************
- * Copyright (C) 2005/06 by the Quassel IRC Team *
+ * Copyright (C) 2005-2020 by the Quassel Project *
* devel@quassel-irc.org *
* *
* This program is free software; you can redistribute it and/or modify *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
- * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
#include "tabcompleter.h"
-TabCompleter::TabCompleter(QLineEdit *l, QObject *parent) : QObject(parent) {
- lineEdit = l;
- enabled = false;
- startOfLineSuffix = QString(": "); // TODO make start of line suffix configurable
-}
+#include <QRegExp>
+
+#include "action.h"
+#include "actioncollection.h"
+#include "buffermodel.h"
+#include "client.h"
+#include "graphicalui.h"
+#include "ircchannel.h"
+#include "ircuser.h"
+#include "multilineedit.h"
+#include "network.h"
+#include "networkmodel.h"
+#include "uisettings.h"
-void TabCompleter::updateNickList(QStringList l) {
- nickList = l;
+const Network* TabCompleter::_currentNetwork;
+BufferId TabCompleter::_currentBufferId;
+QString TabCompleter::_currentBufferName;
+TabCompleter::Type TabCompleter::_completionType;
+
+TabCompleter::TabCompleter(MultiLineEdit* _lineEdit)
+ : QObject(_lineEdit)
+ , _lineEdit(_lineEdit)
+ , _enabled(false)
+ , _nickSuffix(": ")
+{
+ // This Action just serves as a container for the custom shortcut and isn't actually handled;
+ // apparently, using tab as an Action shortcut in an input widget is unreliable on some platforms (e.g. OS/2)
+ _lineEdit->installEventFilter(this);
+ ActionCollection* coll = GraphicalUi::actionCollection("General");
+ QAction* a = coll->addAction("TabCompletionKey",
+ new Action(tr("Tab completion"), coll, this, &TabCompleter::onTabCompletionKey, QKeySequence(Qt::Key_Tab)));
+ a->setEnabled(false); // avoid catching the shortcut
}
-void TabCompleter::updateChannelList(QStringList l) {
- channelList = l;
+void TabCompleter::onTabCompletionKey()
+{
+ // do nothing; we use the event filter instead
}
-void TabCompleter::buildCompletionList() {
- // this is the first time tab is pressed -> build up the completion list and it's iterator
- QString tabAbbrev = lineEdit->text().left(lineEdit->cursorPosition()).section(' ',-1,-1);
- completionList.clear();
- foreach(QString nick, nickList) {
- if(nick.toLower().startsWith(tabAbbrev.toLower())) {
- completionList << nick;
+void TabCompleter::buildCompletionList()
+{
+ // ensure a safe state in case we return early.
+ _completionMap.clear();
+ _nextCompletion = _completionMap.begin();
+
+ // this is the first time tab is pressed -> build up the completion list and it's iterator
+ QModelIndex currentIndex = Client::bufferModel()->currentIndex();
+ _currentBufferId = currentIndex.data(NetworkModel::BufferIdRole).value<BufferId>();
+ if (!_currentBufferId.isValid())
+ return;
+
+ NetworkId networkId = currentIndex.data(NetworkModel::NetworkIdRole).value<NetworkId>();
+ _currentBufferName = currentIndex.sibling(currentIndex.row(), 0).data().toString();
+
+ _currentNetwork = Client::network(networkId);
+ if (!_currentNetwork)
+ return;
+
+ QString tabAbbrev = _lineEdit->text().left(_lineEdit->cursorPosition()).section(QRegExp(R"([^#\w\d-_\[\]{}|`^.\\])"), -1, -1);
+ QRegExp regex(QString(R"(^[-_\[\]{}|`^.\\]*)").append(QRegExp::escape(tabAbbrev)), Qt::CaseInsensitive);
+
+ // channel completion - add all channels of the current network to the map
+ if (tabAbbrev.startsWith('#')) {
+ _completionType = ChannelTab;
+ foreach (IrcChannel* ircChannel, _currentNetwork->ircChannels()) {
+ if (regex.indexIn(ircChannel->name()) > -1)
+ _completionMap[ircChannel->name()] = ircChannel->name();
+ }
}
- }
- completionList.sort();
- nextCompletion = completionList.begin();
- lastCompletionLength = tabAbbrev.length();
+ else {
+ // user completion
+ _completionType = UserTab;
+ switch (static_cast<BufferInfo::Type>(currentIndex.data(NetworkModel::BufferTypeRole).toInt())) {
+ case BufferInfo::ChannelBuffer: { // scope is needed for local var declaration
+ IrcChannel* channel = _currentNetwork->ircChannel(_currentBufferName);
+ if (!channel)
+ return;
+ foreach (IrcUser* ircUser, channel->ircUsers()) {
+ if (regex.indexIn(ircUser->nick()) > -1)
+ _completionMap[ircUser->nick().toLower()] = ircUser->nick();
+ }
+ } break;
+ case BufferInfo::QueryBuffer:
+ if (regex.indexIn(_currentBufferName) > -1)
+ _completionMap[_currentBufferName.toLower()] = _currentBufferName;
+ // fallthrough
+ case BufferInfo::StatusBuffer:
+ if (!_currentNetwork->myNick().isEmpty() && regex.indexIn(_currentNetwork->myNick()) > -1)
+ _completionMap[_currentNetwork->myNick().toLower()] = _currentNetwork->myNick();
+ break;
+ default:
+ return;
+ }
+ }
+
+ _nextCompletion = _completionMap.begin();
+ _lastCompletionLength = tabAbbrev.length();
}
-void TabCompleter::complete() {
- if (! enabled) {
- buildCompletionList();
- enabled = true;
- }
-
- if (nextCompletion != completionList.end()) {
- // clear previous completion
- for (int i = 0; i < lastCompletionLength; i++) {
- lineEdit->backspace();
+void TabCompleter::complete()
+{
+ TabCompletionSettings s;
+ _nickSuffix = s.completionSuffix();
+
+ if (!_enabled) {
+ buildCompletionList();
+ _enabled = true;
}
-
- // insert completion
- lineEdit->insert(*nextCompletion);
-
- // remember charcount to delete next time and advance to next completion
- lastCompletionLength = nextCompletion->length();
- nextCompletion++;
-
- // we're completing the first word of the line
- if(lineEdit->text().length() == lastCompletionLength) {
- lineEdit->insert(startOfLineSuffix);
- lastCompletionLength += 2;
+
+ if (_nextCompletion != _completionMap.end()) {
+ // clear previous completion
+ for (int i = 0; i < _lastCompletionLength; i++) {
+ _lineEdit->backspace();
+ }
+
+ // insert completion
+ _lineEdit->insert(*_nextCompletion);
+
+ // remember charcount to delete next time and advance to next completion
+ _lastCompletionLength = _nextCompletion->length();
+ _nextCompletion++;
+
+ // we're completing the first word of the line
+ if (_completionType == UserTab && _lineEdit->cursorPosition() == _lastCompletionLength) {
+ _lineEdit->insert(_nickSuffix);
+ _lastCompletionLength += _nickSuffix.length();
+ }
+ else if (s.addSpaceMidSentence()) {
+ _lineEdit->addCompletionSpace();
+ _lastCompletionLength++;
+ }
+
+ // we're at the end of the list -> start over again
}
+ else {
+ if (!_completionMap.isEmpty()) {
+ _nextCompletion = _completionMap.begin();
+ complete();
+ }
+ }
+}
- // we're at the end of the list -> start over again
- } else {
- nextCompletion = completionList.begin();
- }
-
+void TabCompleter::reset()
+{
+ _enabled = false;
}
-void TabCompleter::disable() {
- enabled = false;
+bool TabCompleter::eventFilter(QObject* obj, QEvent* event)
+{
+ if (obj != _lineEdit || event->type() != QEvent::KeyPress)
+ return QObject::eventFilter(obj, event);
+
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+
+ if (keyEvent->key() == GraphicalUi::actionCollection("General")->action("TabCompletionKey")->shortcut()[0])
+ complete();
+ else
+ reset();
+
+ return false;
}
+// this determines the sort order
+bool TabCompleter::CompletionKey::operator<(const CompletionKey& other) const
+{
+ switch (_completionType) {
+ case UserTab: {
+ IrcUser* thisUser = _currentNetwork->ircUser(this->contents);
+ if (thisUser && _currentNetwork->isMe(thisUser))
+ return false;
+
+ IrcUser* thatUser = _currentNetwork->ircUser(other.contents);
+ if (thatUser && _currentNetwork->isMe(thatUser))
+ return true;
+
+ if (!thisUser || !thatUser)
+ return QString::localeAwareCompare(this->contents, other.contents) < 0;
+
+ QDateTime thisSpokenTo = thisUser->lastSpokenTo(_currentBufferId);
+ QDateTime thatSpokenTo = thatUser->lastSpokenTo(_currentBufferId);
+
+ if (thisSpokenTo.isValid() || thatSpokenTo.isValid())
+ return thisSpokenTo > thatSpokenTo;
+
+ QDateTime thisTime = thisUser->lastChannelActivity(_currentBufferId);
+ QDateTime thatTime = thatUser->lastChannelActivity(_currentBufferId);
+
+ if (thisTime.isValid() || thatTime.isValid())
+ return thisTime > thatTime;
+ } break;
+ case ChannelTab:
+ if (QString::compare(_currentBufferName, this->contents, Qt::CaseInsensitive) == 0)
+ return true;
+
+ if (QString::compare(_currentBufferName, other.contents, Qt::CaseInsensitive) == 0)
+ return false;
+ break;
+ default:
+ break;
+ }
+
+ return QString::localeAwareCompare(this->contents, other.contents) < 0;
+}