From 087267cdfedeab2ae55776f7b7246c47f1d39d6d Mon Sep 17 00:00:00 2001 From: Kevin Smith 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 + +#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 words_; + String lastCompletion_; + String lastShort_; + std::vector 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 +#include + +#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) { +void MUCController::preHandleIncomingMessage(boost::shared_ptr 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 onClosed; boost::signal 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 onClosed; boost::signal 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 #include #include @@ -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& 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 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