summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Smith <git@kismith.co.uk>2010-06-05 12:30:16 (GMT)
committerKevin Smith <git@kismith.co.uk>2010-06-05 12:30:16 (GMT)
commit087267cdfedeab2ae55776f7b7246c47f1d39d6d (patch)
tree47c5a3bedd1fa7eb61587a7bd17934f0927dbb9f
parent7bca08eb2829982865f1649483f9aa01b3413b1c (diff)
downloadswift-087267cdfedeab2ae55776f7b7246c47f1d39d6d.zip
swift-087267cdfedeab2ae55776f7b7246c47f1d39d6d.tar.bz2
Tab completion in MUCs.
Resolves: #440
-rw-r--r--SwifTools/SConscript1
-rw-r--r--SwifTools/TabComplete.cpp50
-rw-r--r--SwifTools/TabComplete.h25
-rw-r--r--SwifTools/UnitTest/SConscript3
-rw-r--r--SwifTools/UnitTest/TabCompleteTest.cpp152
-rw-r--r--Swift/Controllers/Chat/MUCController.cpp16
-rw-r--r--Swift/Controllers/Chat/MUCController.h2
-rw-r--r--Swift/Controllers/UIInterfaces/ChatWindow.h2
-rw-r--r--Swift/Controllers/UnitTest/MockChatWindow.h1
-rw-r--r--Swift/QtUI/QtChatWindow.cpp31
-rw-r--r--Swift/QtUI/QtChatWindow.h3
-rw-r--r--Swift/QtUI/QtTextEdit.cpp1
12 files changed, 285 insertions, 2 deletions
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 {