From 087267cdfedeab2ae55776f7b7246c47f1d39d6d Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Sat, 5 Jun 2010 13:30:16 +0100
Subject: Tab completion in MUCs.

Resolves: #440

diff --git a/SwifTools/SConscript b/SwifTools/SConscript
index 8da482a..18781c3 100644
--- a/SwifTools/SConscript
+++ b/SwifTools/SConscript
@@ -26,6 +26,7 @@ if env["SCONS_STAGE"] == "build" :
 			"AutoUpdater/AutoUpdater.cpp",
 			"AutoUpdater/PlatformAutoUpdaterFactory.cpp",
 			"Linkify.cpp",
+			"TabComplete.cpp",
 		]
 
 	if myenv.get("HAVE_SPARKLE", 0) :
diff --git a/SwifTools/TabComplete.cpp b/SwifTools/TabComplete.cpp
new file mode 100644
index 0000000..fd3fb6f
--- /dev/null
+++ b/SwifTools/TabComplete.cpp
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "SwifTools/TabComplete.h"
+#include "Swiften/Base/foreach.h"
+
+namespace Swift {
+
+void TabComplete::addWord(const String& word) {
+	words_.push_back(word);
+	if (word.getLowerCase().beginsWith(lastShort_)) {
+		lastCompletionCandidates_.push_back(word);
+	}
+}
+
+void TabComplete::removeWord(const String& word) {
+	words_.erase(std::remove(words_.begin(), words_.end(), word), words_.end());
+	lastCompletionCandidates_.erase(std::remove(lastCompletionCandidates_.begin(), lastCompletionCandidates_.end(), word), lastCompletionCandidates_.end());
+}
+
+String TabComplete::completeWord(const String& word) {
+	if (word == lastCompletion_) {
+		if (lastCompletionCandidates_.size() != 0) {
+			size_t match = 0;
+			for (match = 0; match < lastCompletionCandidates_.size(); match++) {
+				if (lastCompletionCandidates_[match] == lastCompletion_) {
+					break;
+				}
+			}
+			size_t nextIndex = match + 1;
+			nextIndex = nextIndex >= lastCompletionCandidates_.size() ? 0 : nextIndex;
+			lastCompletion_ = lastCompletionCandidates_[nextIndex];
+		}		
+	} else {
+		lastShort_ = word.getLowerCase();
+		lastCompletionCandidates_.clear();
+		foreach (String candidate, words_) {
+			if (candidate.getLowerCase().beginsWith(word.getLowerCase())) {
+				lastCompletionCandidates_.push_back(candidate);
+			}
+		}
+		lastCompletion_ = lastCompletionCandidates_.size() > 0 ? lastCompletionCandidates_[0] : word;
+	}
+	return lastCompletion_;
+}
+
+}
diff --git a/SwifTools/TabComplete.h b/SwifTools/TabComplete.h
new file mode 100644
index 0000000..01e294f
--- /dev/null
+++ b/SwifTools/TabComplete.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "Swiften/Base/String.h"
+
+namespace Swift {
+	class TabComplete {
+		public:
+			void addWord(const String& word);
+			void removeWord(const String& word);
+			String completeWord(const String& word);
+		private:
+			std::vector<String> words_;
+			String lastCompletion_;
+			String lastShort_;
+			std::vector<String> lastCompletionCandidates_;
+	};
+}
diff --git a/SwifTools/UnitTest/SConscript b/SwifTools/UnitTest/SConscript
index 2622f39..8034905 100644
--- a/SwifTools/UnitTest/SConscript
+++ b/SwifTools/UnitTest/SConscript
@@ -1,5 +1,6 @@
 Import("env")
 
 env.Append(UNITTEST_SOURCES = [
-		File("LinkifyTest.cpp")
+		File("LinkifyTest.cpp"),
+		File("TabCompleteTest.cpp")
 	])
diff --git a/SwifTools/UnitTest/TabCompleteTest.cpp b/SwifTools/UnitTest/TabCompleteTest.cpp
new file mode 100644
index 0000000..4d152f6
--- /dev/null
+++ b/SwifTools/UnitTest/TabCompleteTest.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include "SwifTools/TabComplete.h"
+
+using namespace Swift;
+
+class TabCompleteTest : public CppUnit::TestFixture {
+	CPPUNIT_TEST_SUITE(TabCompleteTest);
+	CPPUNIT_TEST(testEmpty);
+	CPPUNIT_TEST(testNoMatch);
+	CPPUNIT_TEST(testOneMatch);
+	CPPUNIT_TEST(testTwoMatch);
+	CPPUNIT_TEST(testChangeMatch);
+	CPPUNIT_TEST(testRemoveDuringComplete);
+	CPPUNIT_TEST(testAddDuringComplete);
+	CPPUNIT_TEST_SUITE_END();
+	
+public:
+	TabCompleteTest() {};
+	
+	void setUp() {
+		completer_ = TabComplete();
+	}
+	
+	void testEmpty() {
+		String blah("Blah");
+		CPPUNIT_ASSERT_EQUAL(
+			blah,
+			completer_.completeWord(blah));
+		CPPUNIT_ASSERT_EQUAL(
+			blah,
+			completer_.completeWord(blah));
+	}
+
+	void testNoMatch() {
+		completer_.addWord("Bleh");
+		String blah("Blah");
+		CPPUNIT_ASSERT_EQUAL(
+			blah,
+			completer_.completeWord(blah));
+		CPPUNIT_ASSERT_EQUAL(
+			blah,
+			completer_.completeWord(blah));
+	}
+
+	void testOneMatch() {
+		String short1("Bl");
+		String long1("Blehling");
+		completer_.addWord(long1);
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(long1));
+	}
+	
+	void testTwoMatch() {
+		String short1("Hur");
+		String long1("Hurgle");
+		String long2("Hurdler");
+		completer_.addWord(long1);
+		completer_.addWord("Blah");
+		completer_.addWord(long2);
+		completer_.addWord("Bleh");
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(long1));
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(long2));
+	}
+
+	void testChangeMatch() {
+		String short1("Hur");
+		String short2("Rub");
+		String long1("Hurgle");
+		String long2("Rubbish");
+		completer_.addWord(long2);
+		completer_.addWord("Blah");
+		completer_.addWord(long1);
+		completer_.addWord("Bleh");
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(short2));
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(long2));
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+	}
+
+	void testRemoveDuringComplete() {
+		String short1("Kev");
+		String long1("Kevin");
+		String long2("Kevlar");
+		completer_.addWord(long1);
+		completer_.addWord("Blah");
+		completer_.addWord(long2);
+		completer_.addWord("Bleh");
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+		completer_.removeWord(long1);
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(long1));
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(long2));
+	}
+
+	void testAddDuringComplete() {
+		String short1("Rem");
+		String long1("Remko");
+		String long2("Remove");
+		String long3("Remedial");
+		completer_.addWord(long1);
+		completer_.addWord("Blah");
+		completer_.addWord(long2);
+		completer_.addWord("Bleh");
+		CPPUNIT_ASSERT_EQUAL(
+			long1,
+			completer_.completeWord(short1));
+		completer_.addWord(long3);
+		CPPUNIT_ASSERT_EQUAL(
+			long2,
+			completer_.completeWord(long1));
+		CPPUNIT_ASSERT_EQUAL(
+			long3,
+			completer_.completeWord(long2));
+	}
+
+private:
+	TabComplete completer_;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(TabCompleteTest);
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index 14fe180..65603d7 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -10,6 +10,7 @@
 
 #include "Swiften/Network/Timer.h"
 #include "Swiften/Network/TimerFactory.h"
+#include "SwifTools/TabComplete.h"
 #include "Swiften/Base/foreach.h"
 #include "Swift/Controllers/UIInterfaces/ChatWindow.h"
 #include "Swift/Controllers/UIInterfaces/ChatWindowFactory.h"
@@ -49,7 +50,9 @@ MUCController::MUCController (
 	events_ = uiEventStream;
 	
 	roster_ = new Roster(true);
+	completer_ = new TabComplete();
 	chatWindow_->setRosterModel(roster_);
+	chatWindow_->setTabComplete(completer_);
 	chatWindow_->onClosed.connect(boost::bind(&MUCController::handleWindowClosed, this));
 	muc_->onJoinComplete.connect(boost::bind(&MUCController::handleJoinComplete, this, _1));
 	muc_->onJoinFailed.connect(boost::bind(&MUCController::handleJoinFailed, this, _1));
@@ -79,6 +82,8 @@ MUCController::~MUCController() {
 	if (loginCheckTimer_) {
 		loginCheckTimer_->stop();
 	}
+	chatWindow_->setTabComplete(NULL);
+	delete completer_;
 }
 
 void MUCController::handleJoinTimeoutTick() {
@@ -139,6 +144,9 @@ void MUCController::handleWindowClosed() {
 }
 
 void MUCController::handleOccupantJoined(const MUCOccupant& occupant) {
+	if (nick_ != occupant.getNick()) {
+		completer_->addWord(occupant.getNick());
+	}
 	receivedActivity();
 	JID jid(nickToJID(occupant.getNick()));
 	JID realJID;
@@ -175,7 +183,12 @@ JID MUCController::nickToJID(const String& nick) {
 	return JID(toJID_.getNode(), toJID_.getDomain(), nick);
 }
 
-void MUCController::preHandleIncomingMessage(boost::shared_ptr<Message>) {
+void MUCController::preHandleIncomingMessage(boost::shared_ptr<Message> message) {
+	String nick = message->getFrom().getResource();
+	if (nick != nick_) {
+		completer_->removeWord(nick);
+		completer_->addWord(nick);
+	}
 	/*Buggy implementations never send the status code, so use an incoming message as a hint that joining's done (e.g. the old ejabberd on psi-im.org).*/
 	receivedActivity();
 	joined_ = true;
@@ -206,6 +219,7 @@ String MUCController::roleToGroupName(MUCOccupant::Role role) {
 }
 
 void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::LeavingType, const String& reason) {
+	completer_->removeWord(occupant.getNick());
 	String partMessage = occupant.getNick() + " has left the room";
 	if (!reason.isEmpty()) {
 		partMessage += " (" + reason + ")";
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index f409309..a3c72e7 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -28,6 +28,7 @@ namespace Swift {
 	class AvatarManager;
 	class UIEventStream;
 	class TimerFactory;
+	class TabComplete;
 
 	class MUCController : public ChatControllerBase {
 		public:
@@ -60,6 +61,7 @@ namespace Swift {
 			UIEventStream* events_;
 			String nick_;
 			Roster* roster_;
+			TabComplete* completer_;
 			bool parting_;
 			bool joined_;
 			boost::bsignals::scoped_connection avatarChangedConnection_;
diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h
index 7beb074..7e97f0c 100644
--- a/Swift/Controllers/UIInterfaces/ChatWindow.h
+++ b/Swift/Controllers/UIInterfaces/ChatWindow.h
@@ -20,6 +20,7 @@ namespace Swift {
 	class AvatarManager;
 	class TreeWidget;
 	class Roster;
+	class TabComplete;
 
 	class ChatWindow {
 		public:
@@ -44,6 +45,7 @@ namespace Swift {
 			virtual SecurityLabel getSelectedSecurityLabel() = 0;
 			virtual void setInputEnabled(bool enabled) = 0;
 			virtual void setRosterModel(Roster* model) = 0;
+			virtual void setTabComplete(TabComplete* completer) = 0;
 
 			boost::signal<void ()> onClosed;
 			boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h
index 1ce87ff..78705ce 100644
--- a/Swift/Controllers/UnitTest/MockChatWindow.h
+++ b/Swift/Controllers/UnitTest/MockChatWindow.h
@@ -31,6 +31,7 @@ namespace Swift {
 			virtual SecurityLabel getSelectedSecurityLabel() {return SecurityLabel();};
 			virtual void setInputEnabled(bool /*enabled*/) {};
 			virtual void setRosterModel(Roster* /*roster*/) {};
+			virtual void setTabComplete(TabComplete* complete) {};
 
 			boost::signal<void ()> onClosed;
 			boost::signal<void ()> onAllMessagesRead;
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index cb4c437..6f49082 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -14,6 +14,8 @@
 #include "SystemMessageSnippet.h"
 #include "QtTextEdit.h"
 
+#include "SwifTools/TabComplete.h"
+
 #include <QApplication>
 #include <QBoxLayout>
 #include <QCloseEvent>
@@ -80,6 +82,10 @@ QtChatWindow::~QtChatWindow() {
 
 }
 
+void QtChatWindow::setTabComplete(TabComplete* completer) {
+	completer_ = completer;
+}
+
 void QtChatWindow::handleKeyPressEvent(QKeyEvent* event) {
 	int key = event->key();
 	Qt::KeyboardModifiers modifiers = event->modifiers();
@@ -95,11 +101,36 @@ void QtChatWindow::handleKeyPressEvent(QKeyEvent* event) {
 		|| (key == Qt::Key_Right && modifiers == (Qt::ControlModifier & Qt::ShiftModifier))
 	) {
 		emit requestNextTab();
+	} else if (key == Qt::Key_Tab) {
+		tabComplete();
 	} else {
 		messageLog_->handleKeyPressEvent(event);
 	}
 }
 
+void QtChatWindow::tabComplete() {
+	if (!completer_) {
+		return;
+	}
+//	QTextDocument* document = input_->document();
+	QTextCursor cursor = input_->textCursor();
+	cursor.select(QTextCursor::WordUnderCursor);
+	QString root = cursor.selectedText();
+	bool firstWord = cursor.selectionStart() == 0;
+	QString suggestion = P2QSTRING(completer_->completeWord(Q2PSTRING(root)));
+	if (root == suggestion) {
+		return;
+	}
+	cursor.beginEditBlock();
+	cursor.removeSelectedText();
+	cursor.insertText(suggestion);
+	if (firstWord) {
+		//	cursor.insertText(":");
+	}
+	//cursor.insertText(" ");
+	cursor.endEditBlock();
+}
+
 void QtChatWindow::setRosterModel(Roster* roster) {
 	treeWidget_->setRosterModel(roster);	
 }
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index 9d3e3a7..ff2f1cb 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -45,6 +45,7 @@ namespace Swift {
 			QtTabbable::AlertType getWidgetAlertState();
 			void setContactChatState(ChatState::ChatStateType state);
 			void setRosterModel(Roster* roster);
+			void setTabComplete(TabComplete* completer);
 
 		protected slots:
 			void qAppFocusChanged(QWidget* old, QWidget* now);
@@ -59,6 +60,7 @@ namespace Swift {
 
 		private:
 			void updateTitleWithUnreadCount();
+			void tabComplete();
 			void addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath, const QString& style);
 
 			int unreadCount_;
@@ -68,6 +70,7 @@ namespace Swift {
 			QtTextEdit* input_;
 			QComboBox *labelsWidget_;
 			QtTreeWidget *treeWidget_;
+			TabComplete* completer_;
 			std::vector<SecurityLabel> availableLabels_;
 			bool previousMessageWasSelf_;
 			bool previousMessageWasSystem_;
diff --git a/Swift/QtUI/QtTextEdit.cpp b/Swift/QtUI/QtTextEdit.cpp
index 451a18c..89fc68f 100644
--- a/Swift/QtUI/QtTextEdit.cpp
+++ b/Swift/QtUI/QtTextEdit.cpp
@@ -29,6 +29,7 @@ void QtTextEdit::keyPressEvent(QKeyEvent* event) {
 			   || (key == Qt::Key_PageDown && modifiers == Qt::ControlModifier)
 			   || (key == Qt::Key_Left && modifiers == (Qt::ControlModifier | Qt::ShiftModifier))
 			   || (key == Qt::Key_Right && modifiers == (Qt::ControlModifier | Qt::ShiftModifier))
+			   || (key == Qt::Key_Tab)
 	) {
 		emit unhandledKeyPressEvent(event);
 	} else {
-- 
cgit v0.10.2-6-g49f6