diff options
author | Kevin Smith <git@kismith.co.uk> | 2010-11-18 12:09:08 (GMT) |
---|---|---|
committer | Kevin Smith <git@kismith.co.uk> | 2010-12-22 20:19:07 (GMT) |
commit | 9f04a7ec3429303118f12607703b877d8ba43888 (patch) | |
tree | 3e868395fa3c4f9cbbf84614094bb0251b425c15 | |
parent | 04b99ce8430109d0467c29c85cb6bacb85c54c44 (diff) | |
download | swift-contrib-9f04a7ec3429303118f12607703b877d8ba43888.zip swift-contrib-9f04a7ec3429303118f12607703b877d8ba43888.tar.bz2 |
Basic User Search support, and Find Rooms cleanup.
Adds a throbber to the MUC search, turns the Add Contact dialog into something searchy, adds the option to open chats to arbitrary JIDs.
Resolves: #614
Resolves: #695
Resolves: #436
Release-Notes: On servers that support it, users can now perform searches for contacts to add or chat to.
32 files changed, 1269 insertions, 104 deletions
diff --git a/Swift/Controllers/Chat/MUCSearchController.cpp b/Swift/Controllers/Chat/MUCSearchController.cpp index 7368dbb..c254e51 100644 --- a/Swift/Controllers/Chat/MUCSearchController.cpp +++ b/Swift/Controllers/Chat/MUCSearchController.cpp @@ -11,22 +11,26 @@ #include <boost/bind.hpp> #include <boost/shared_ptr.hpp> -#include "Swiften/Disco/GetDiscoInfoRequest.h" -#include "Swiften/Disco/GetDiscoItemsRequest.h" +#include <Swiften/Disco/GetDiscoInfoRequest.h> +#include <Swiften/Disco/GetDiscoItemsRequest.h> -#include "Swift/Controllers/UIEvents/UIEventStream.h" -#include "Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h" -#include "Swift/Controllers/UIInterfaces/MUCSearchWindow.h" -#include "Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h" +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h> +#include <Swift/Controllers/UIInterfaces/MUCSearchWindow.h> +#include <Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h> +#include <Swift/Controllers/DiscoServiceWalker.h> +#include <Swiften/Client/NickResolver.h> namespace Swift { static const String SEARCHED_SERVICES = "searchedServices"; -MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* factory, IQRouter* iqRouter, SettingsProvider* settings) : jid_(jid) { +MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* factory, IQRouter* iqRouter, SettingsProvider* settings, NickResolver *nickResolver) : jid_(jid) { iqRouter_ = iqRouter; settings_ = settings; uiEventStream_ = uiEventStream; + nickResolver_ = nickResolver; + itemsInProgress_ = 0; uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&MUCSearchController::handleUIEvent, this, _1)); window_ = NULL; factory_ = factory; @@ -34,6 +38,9 @@ MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventS } MUCSearchController::~MUCSearchController() { + foreach (DiscoServiceWalker* walker, walksInProgress_) { + delete walker; + } delete window_; } @@ -42,12 +49,12 @@ void MUCSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) { if (searchEvent) { if (!window_) { window_ = factory_->createMUCSearchWindow(uiEventStream_); - window_->onAddService.connect(boost::bind(&MUCSearchController::handleAddService, this, _1, true)); + window_->onAddService.connect(boost::bind(&MUCSearchController::handleAddService, this, _1)); window_->addSavedServices(savedServices_); - handleAddService(JID(jid_.getDomain()), true); + handleAddService(JID(jid_.getDomain())); } window_->setMUC(""); - window_->setNick(jid_.getNode()); + window_->setNick(nickResolver_->jidToNick(jid_)); window_->show(); return; } @@ -79,76 +86,66 @@ void MUCSearchController::addAndSaveServices(const JID& jid) { window_->addSavedServices(savedServices_); } -void MUCSearchController::handleAddService(const JID& jid, bool userTriggered) { - if (userTriggered) { - addAndSaveServices(jid); - } - if (std::find(services_.begin(), services_.end(), jid) != services_.end()) { - if (!userTriggered) { - /* No infinite recursion. (Some buggy servers do infinitely deep disco of themselves)*/ - return; - } - } else if (userTriggered) { - services_.push_back(jid); - serviceDetails_[jid].setComplete(false); - refreshView(); - } +void MUCSearchController::handleAddService(const JID& jid) { if (!jid.isValid()) { //Set Window to say error this isn't valid return; } - GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(jid, iqRouter_); - discoInfoRequest->onResponse.connect(boost::bind(&MUCSearchController::handleDiscoInfoResponse, this, _1, _2, jid)); - discoInfoRequest->send(); -} - -void MUCSearchController::removeService(const JID& jid) { - serviceDetails_.erase(jid); - services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); + addAndSaveServices(jid); + services_.push_back(jid); + serviceDetails_[jid].setComplete(false); + window_->setSearchInProgress(true); refreshView(); + DiscoServiceWalker* walker = new DiscoServiceWalker(jid, iqRouter_); + walker->onServiceFound.connect(boost::bind(&MUCSearchController::handleDiscoServiceFound, this, _1, _2)); + walker->onWalkComplete.connect(boost::bind(&MUCSearchController::handleDiscoWalkFinished, this, walker)); + walksInProgress_.push_back(walker); + walker->beginWalk(); } -void MUCSearchController::handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid) { - if (error) { - handleDiscoError(jid, error); - return; - } - GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_); - bool mucService = false; - bool couldContainServices = false; +void MUCSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info) { + bool isMUC; String name; foreach (DiscoInfo::Identity identity, info->getIdentities()) { - if ((identity.getCategory() == "directory" - && identity.getType() == "chatroom") - || (identity.getCategory() == "conference" - && identity.getType() == "text")) { - mucService = true; - name = identity.getName(); - } - if (identity.getCategory() == "server") { - couldContainServices = true; - name = identity.getName(); - } + if ((identity.getCategory() == "directory" + && identity.getType() == "chatroom") + || (identity.getCategory() == "conference" + && identity.getType() == "text")) { + isMUC = true; + name = identity.getName(); + } } - services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); /* Bring it back to the end on a refresh */ - services_.push_back(jid); - serviceDetails_[jid].setName(name); - serviceDetails_[jid].setJID(jid); - serviceDetails_[jid].setComplete(false); - - if (mucService) { + if (isMUC) { + services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); /* Bring it back to the end on a refresh */ + services_.push_back(jid); + serviceDetails_[jid].setName(name); + serviceDetails_[jid].setJID(jid); + serviceDetails_[jid].setComplete(false); + itemsInProgress_++; + GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_); discoItemsRequest->onResponse.connect(boost::bind(&MUCSearchController::handleRoomsItemsResponse, this, _1, _2, jid)); - } else if (couldContainServices) { - discoItemsRequest->onResponse.connect(boost::bind(&MUCSearchController::handleServerItemsResponse, this, _1, _2, jid)); + discoItemsRequest->send(); } else { removeService(jid); - return; } - discoItemsRequest->send(); + refreshView(); +} + +void MUCSearchController::handleDiscoWalkFinished(DiscoServiceWalker* walker) { + walksInProgress_.erase(std::remove(walksInProgress_.begin(), walksInProgress_.end(), walker), walksInProgress_.end()); + updateInProgressness(); + delete walker; +} + +void MUCSearchController::removeService(const JID& jid) { + serviceDetails_.erase(jid); + services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); refreshView(); } void MUCSearchController::handleRoomsItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) { + itemsInProgress_--; + updateInProgressness(); if (error) { handleDiscoError(jid, error); return; @@ -161,25 +158,6 @@ void MUCSearchController::handleRoomsItemsResponse(boost::shared_ptr<DiscoItems> refreshView(); } -void MUCSearchController::handleServerItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) { - if (error) { - handleDiscoError(jid, error); - return; - } - if (jid.isValid()) { - removeService(jid); - } - foreach (DiscoItems::Item item, items->getItems()) { - if (item.getNode().isEmpty()) { - /* Don't look at noded items. It's possible that this will exclude some services, - * but I've never seen one in the wild, and it's an easy fix for not looping. - */ - handleAddService(item.getJID()); - } - } - refreshView(); -} - void MUCSearchController::handleDiscoError(const JID& jid, ErrorPayload::ref error) { serviceDetails_[jid].setComplete(true); serviceDetails_[jid].setError(error->getText()); @@ -192,4 +170,8 @@ void MUCSearchController::refreshView() { } } +void MUCSearchController::updateInProgressness() { + window_->setSearchInProgress(walksInProgress_.size() + itemsInProgress_ > 0); +} + } diff --git a/Swift/Controllers/Chat/MUCSearchController.h b/Swift/Controllers/Chat/MUCSearchController.h index f09a801..6caee54 100644 --- a/Swift/Controllers/Chat/MUCSearchController.h +++ b/Swift/Controllers/Chat/MUCSearchController.h @@ -27,6 +27,8 @@ namespace Swift { class MUCSearchWindow; class MUCSearchWindowFactory; class IQRouter; + class DiscoServiceWalker; + class NickResolver; class MUCService { public: @@ -86,28 +88,32 @@ namespace Swift { class MUCSearchController { public: - MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* mucSearchWindowFactory, IQRouter* iqRouter, SettingsProvider* settings); + MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* mucSearchWindowFactory, IQRouter* iqRouter, SettingsProvider* settings, NickResolver* nickResolver); ~MUCSearchController(); private: void handleUIEvent(boost::shared_ptr<UIEvent> event); - void handleAddService(const JID& jid, bool userTriggered=false); - void handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid); + void handleAddService(const JID& jid); void handleRoomsItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid); - void handleServerItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid); void handleDiscoError(const JID& jid, ErrorPayload::ref error); + void handleDiscoServiceFound(const JID&, boost::shared_ptr<DiscoInfo>); + void handleDiscoWalkFinished(DiscoServiceWalker* walker); void removeService(const JID& jid); void refreshView(); void loadServices(); void addAndSaveServices(const JID& jid); + void updateInProgressness(); UIEventStream* uiEventStream_; MUCSearchWindow* window_; MUCSearchWindowFactory* factory_; SettingsProvider* settings_; + NickResolver* nickResolver_; boost::bsignals::scoped_connection uiEventConnection_; std::vector<JID> services_; std::vector<JID> savedServices_; std::map<JID, MUCService> serviceDetails_; + std::vector<DiscoServiceWalker*> walksInProgress_; IQRouter* iqRouter_; JID jid_; + int itemsInProgress_; }; } diff --git a/Swift/Controllers/Chat/UserSearchController.cpp b/Swift/Controllers/Chat/UserSearchController.cpp new file mode 100644 index 0000000..c3e40c8 --- /dev/null +++ b/Swift/Controllers/Chat/UserSearchController.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swift/Controllers/Chat/UserSearchController.h" + +#include <boost/bind.hpp> +#include <boost/shared_ptr.hpp> + +#include <Swiften/Disco/GetDiscoInfoRequest.h> +#include <Swiften/Disco/GetDiscoItemsRequest.h> + +#include <Swift/Controllers/DiscoServiceWalker.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> + +namespace Swift { +UserSearchController::UserSearchController(const JID& jid, UIEventStream* uiEventStream, UserSearchWindowFactory* factory, IQRouter* iqRouter) : jid_(jid) { + iqRouter_ = iqRouter; + uiEventStream_ = uiEventStream; + uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&UserSearchController::handleUIEvent, this, _1)); + window_ = NULL; + factory_ = factory; + discoWalker_ = NULL; +} + +UserSearchController::~UserSearchController() { + delete window_; + delete discoWalker_; +} + +void UserSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) { + boost::shared_ptr<RequestUserSearchUIEvent> searchEvent = boost::dynamic_pointer_cast<RequestUserSearchUIEvent>(event); + if (searchEvent) { + if (!window_) { + window_ = factory_->createUserSearchWindow(uiEventStream_); + window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); + window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); + window_->setSelectedService(JID(jid_.getDomain())); + window_->clear(); + } + window_->show(); + return; + } +} + +void UserSearchController::handleFormRequested(const JID& service) { + window_->setSearchError(false); + window_->setServerSupportsSearch(true); + //Abort a previous search if is active + delete discoWalker_; + discoWalker_ = new DiscoServiceWalker(service, iqRouter_); + discoWalker_->onServiceFound.connect(boost::bind(&UserSearchController::handleDiscoServiceFound, this, _1, _2, discoWalker_)); + discoWalker_->onWalkComplete.connect(boost::bind(&UserSearchController::handleDiscoWalkFinished, this, discoWalker_)); + discoWalker_->beginWalk(); +} + +void UserSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info, DiscoServiceWalker* walker) { + bool isUserDirectory = false; + bool supports55 = false; + foreach (DiscoInfo::Identity identity, info->getIdentities()) { + if ((identity.getCategory() == "directory" + && identity.getType() == "user")) { + isUserDirectory = true; + } + } + std::vector<String> features = info->getFeatures(); + supports55 = std::find(features.begin(), features.end(), DiscoInfo::JabberSearchFeature) != features.end(); + if (/*isUserDirectory && */supports55) { //FIXME: once M-Link correctly advertises directoryness. + /* Abort further searches.*/ + delete discoWalker_; + discoWalker_ = NULL; + boost::shared_ptr<GenericRequest<SearchPayload> > searchRequest(new GenericRequest<SearchPayload>(IQ::Get, jid, boost::shared_ptr<SearchPayload>(new SearchPayload()), iqRouter_)); + searchRequest->onResponse.connect(boost::bind(&UserSearchController::handleFormResponse, this, _1, _2, jid)); + searchRequest->send(); + } +} + +void UserSearchController::handleFormResponse(boost::shared_ptr<SearchPayload> fields, ErrorPayload::ref error, const JID& jid) { + if (error || !fields) { + window_->setServerSupportsSearch(false); + return; + } + window_->setSearchFields(fields); +} + +void UserSearchController::handleSearch(boost::shared_ptr<SearchPayload> fields, const JID& jid) { + boost::shared_ptr<GenericRequest<SearchPayload> > searchRequest(new GenericRequest<SearchPayload>(IQ::Set, jid, fields, iqRouter_)); + searchRequest->onResponse.connect(boost::bind(&UserSearchController::handleSearchResponse, this, _1, _2, jid)); + searchRequest->send(); +} + +void UserSearchController::handleSearchResponse(boost::shared_ptr<SearchPayload> resultsPayload, ErrorPayload::ref error, const JID& jid) { + if (error || !resultsPayload) { + window_->setSearchError(true); + return; + } + std::vector<UserSearchResult> results; + foreach (SearchPayload::Item item, resultsPayload->getItems()) { + JID jid(item.jid); + std::map<String, String> fields; + fields["first"] = item.first; + fields["last"] = item.last; + fields["nick"] = item.nick; + fields["email"] = item.email; + UserSearchResult result(jid, fields); + results.push_back(result); + } + window_->setResults(results); +} + +void UserSearchController::handleDiscoWalkFinished(DiscoServiceWalker* walker) { + window_->setServerSupportsSearch(false); + delete discoWalker_; + discoWalker_ = NULL; +} + +} diff --git a/Swift/Controllers/Chat/UserSearchController.h b/Swift/Controllers/Chat/UserSearchController.h new file mode 100644 index 0000000..3ba3352 --- /dev/null +++ b/Swift/Controllers/Chat/UserSearchController.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <boost/shared_ptr.hpp> +#include <map> +#include <vector> +#include <Swiften/Base/boost_bsignals.h> + +#include <Swiften/Elements/SearchPayload.h> +#include <Swiften/Base/String.h> +#include <Swiften/JID/JID.h> +#include <Swiften/Elements/DiscoInfo.h> +#include <Swiften/Elements/DiscoItems.h> +#include <Swiften/Elements/ErrorPayload.h> + +namespace Swift { + class UIEventStream; + class UIEvent; + class UserSearchWindow; + class UserSearchWindowFactory; + class IQRouter; + class DiscoServiceWalker; + + class UserSearchResult { + public: + UserSearchResult(const JID& jid, const std::map<String, String>& fields) : jid_(jid), fields_(fields) {} + const JID& getJID() const {return jid_;} + const std::map<String, String>& getFields() const {return fields_;} + private: + JID jid_; + std::map<String, String> fields_; + }; + + class UserSearchController { + public: + UserSearchController(const JID& jid, UIEventStream* uiEventStream, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter); + ~UserSearchController(); + private: + void handleUIEvent(boost::shared_ptr<UIEvent> event); + void handleFormRequested(const JID& service); + void handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info, DiscoServiceWalker* walker); + void handleDiscoWalkFinished(DiscoServiceWalker* walker); + void handleFormResponse(boost::shared_ptr<SearchPayload> items, ErrorPayload::ref error, const JID& jid); + void handleSearch(boost::shared_ptr<SearchPayload> fields, const JID& jid); + void handleSearchResponse(boost::shared_ptr<SearchPayload> results, ErrorPayload::ref error, const JID& jid); + UIEventStream* uiEventStream_; + UserSearchWindow* window_; + UserSearchWindowFactory* factory_; + boost::bsignals::scoped_connection uiEventConnection_; + IQRouter* iqRouter_; + JID jid_; + DiscoServiceWalker* discoWalker_; + }; +} diff --git a/Swift/Controllers/DiscoServiceWalker.cpp b/Swift/Controllers/DiscoServiceWalker.cpp new file mode 100644 index 0000000..95ce23b --- /dev/null +++ b/Swift/Controllers/DiscoServiceWalker.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <Swift/Controllers/DiscoServiceWalker.h> + +#include <boost/bind.hpp> + +#include "Swiften/Disco/GetDiscoInfoRequest.h" +#include "Swiften/Disco/GetDiscoItemsRequest.h" + +namespace Swift { + +DiscoServiceWalker::DiscoServiceWalker(const JID& service, IQRouter* iqRouter, size_t maxSteps) : service_(service), iqRouter_(iqRouter), maxSteps_(maxSteps) { + +} + +void DiscoServiceWalker::beginWalk() { + assert(servicesBeingSearched_.size() == 0); + walkNode(service_); +} + +void DiscoServiceWalker::walkNode(const JID& jid) { + servicesBeingSearched_.push_back(jid); + searchedServices_.push_back(jid); + GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(jid, iqRouter_); + discoInfoRequest->onResponse.connect(boost::bind(&DiscoServiceWalker::handleDiscoInfoResponse, this, _1, _2, jid)); + discoInfoRequest->send(); +} + +void DiscoServiceWalker::handleReceivedDiscoItem(const JID& item) { + if (std::find(searchedServices_.begin(), searchedServices_.end(), item) != searchedServices_.end()) { + /* Don't recurse infinitely */ + return; + } + walkNode(item); +} + +void DiscoServiceWalker::handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid) { + if (error) { + handleDiscoError(jid, error); + return; + } + + bool couldContainServices = false; + foreach (DiscoInfo::Identity identity, info->getIdentities()) { + if (identity.getCategory() == "server") { + couldContainServices = true; + } + } + bool completed = false; + if (couldContainServices) { + GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_); + discoItemsRequest->onResponse.connect(boost::bind(&DiscoServiceWalker::handleDiscoItemsResponse, this, _1, _2, jid)); + discoItemsRequest->send(); + } else { + completed = true; + } + onServiceFound(jid, info); + if (completed) { + markNodeCompleted(jid); + } +} + +void DiscoServiceWalker::handleDiscoItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) { + if (error) { + handleDiscoError(jid, error); + return; + } + foreach (DiscoItems::Item item, items->getItems()) { + if (item.getNode().isEmpty()) { + /* Don't look at noded items. It's possible that this will exclude some services, + * but I've never seen one in the wild, and it's an easy fix for not looping. + */ + handleReceivedDiscoItem(item.getJID()); + } + } + markNodeCompleted(jid); +} + +void DiscoServiceWalker::handleDiscoError(const JID& jid, ErrorPayload::ref /*error*/) { + markNodeCompleted(jid); +} + +void DiscoServiceWalker::markNodeCompleted(const JID& jid) { + servicesBeingSearched_.erase(std::remove(servicesBeingSearched_.begin(), servicesBeingSearched_.end(), jid), servicesBeingSearched_.end()); + /* All results are in */ + if (servicesBeingSearched_.size() == 0) { + onWalkComplete(); + } + /* Check if we're on a rampage */ + if (searchedServices_.size() >= maxSteps_) { + onWalkComplete(); + } +} + +} diff --git a/Swift/Controllers/DiscoServiceWalker.h b/Swift/Controllers/DiscoServiceWalker.h new file mode 100644 index 0000000..5b2a47e --- /dev/null +++ b/Swift/Controllers/DiscoServiceWalker.h @@ -0,0 +1,47 @@ +/* + * 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 <boost/shared_ptr.hpp> +#include <Swiften/Base/boost_bsignals.h> +#include <Swiften/Base/String.h> +#include <Swiften/JID/JID.h> +#include <Swiften/Elements/DiscoInfo.h> +#include <Swiften/Elements/DiscoItems.h> +#include <Swiften/Elements/ErrorPayload.h> + +namespace Swift { + class IQRouter; + /** + * Recursively walk service discovery trees to find all services offered. + * This stops on any disco item that's not reporting itself as a server. + */ + class DiscoServiceWalker : public boost::signals::trackable { + public: + DiscoServiceWalker(const JID& service, IQRouter* iqRouter, size_t maxSteps = 200); + /** Start the walk. Call this exactly once.*/ + void beginWalk(); + /** Emitted for each service found. */ + boost::signal<void(const JID&, boost::shared_ptr<DiscoInfo>)> onServiceFound; + /** Emitted when walking is complete.*/ + boost::signal<void()> onWalkComplete; + private: + void handleReceivedDiscoItem(const JID& item); + void walkNode(const JID& jid); + void markNodeCompleted(const JID& jid); + void handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid); + void handleDiscoItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid); + void handleDiscoError(const JID& jid, ErrorPayload::ref error); + JID service_; + IQRouter* iqRouter_; + size_t maxSteps_; + std::vector<JID> servicesBeingSearched_; + std::vector<JID> searchedServices_; + }; +} diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp index 6e57a8f..b925857 100644 --- a/Swift/Controllers/MainController.cpp +++ b/Swift/Controllers/MainController.cpp @@ -18,6 +18,7 @@ #include "Swiften/Client/Storages.h" #include "Swiften/VCards/VCardManager.h" #include "Swift/Controllers/Chat/MUCSearchController.h" +#include "Swift/Controllers/Chat/UserSearchController.h" #include "Swift/Controllers/Chat/ChatsManager.h" #include "Swift/Controllers/XMPPEvents/EventController.h" #include "Swift/Controllers/EventWindowController.h" @@ -98,6 +99,7 @@ MainController::MainController( chatsManager_ = NULL; eventWindowController_ = NULL; mucSearchController_ = NULL; + userSearchController_ = NULL; quitRequested_ = false; timeBeforeNextReconnect_ = -1; @@ -188,6 +190,8 @@ void MainController::resetClient() { statusTracker_ = NULL; delete profileSettings_; profileSettings_ = NULL; + delete userSearchController_; + userSearchController_ = NULL; } void MainController::handleUIEvent(boost::shared_ptr<UIEvent> event) { @@ -242,7 +246,9 @@ void MainController::handleConnected() { client_->getDiscoManager()->setCapsNode(CLIENT_NODE); client_->getDiscoManager()->setDiscoInfo(discoInfo); - mucSearchController_ = new MUCSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), settings_); + + mucSearchController_ = new MUCSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), settings_, client_->getNickResolver()); + userSearchController_ = new UserSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter()); } client_->requestRoster(); diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h index 2f101a5..aece80f 100644 --- a/Swift/Controllers/MainController.h +++ b/Swift/Controllers/MainController.h @@ -54,6 +54,7 @@ namespace Swift { class EventWindowFactory; class EventWindowController; class MUCSearchController; + class UserSearchController; class StatusTracker; class Dock; class Storages; @@ -137,6 +138,7 @@ namespace Swift { boost::shared_ptr<ErrorEvent> lastDisconnectError_; bool useDelayForLatency_; MUCSearchController* mucSearchController_; + UserSearchController* userSearchController_; int timeBeforeNextReconnect_; Timer::ref reconnectTimer_; StatusTracker* statusTracker_; diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript index 9cd2be4..96674bd 100644 --- a/Swift/Controllers/SConscript +++ b/Swift/Controllers/SConscript @@ -26,6 +26,8 @@ if env["SCONS_STAGE"] == "build" : "Chat/ChatsManager.cpp", "Chat/MUCController.cpp", "Chat/MUCSearchController.cpp", + "Chat/UserSearchController.cpp", + "DiscoServiceWalker.cpp", "MainController.cpp", "RosterController.cpp", "RosterGroupExpandinessPersister.cpp", diff --git a/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h b/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h new file mode 100644 index 0000000..1312a84 --- /dev/null +++ b/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include "Swift/Controllers/UIEvents/UIEvent.h" + +namespace Swift { + class RequestUserSearchUIEvent : public UIEvent { + + }; +} diff --git a/Swift/Controllers/UIInterfaces/MUCSearchWindow.h b/Swift/Controllers/UIInterfaces/MUCSearchWindow.h index da54ded..3c0ab12 100644 --- a/Swift/Controllers/UIInterfaces/MUCSearchWindow.h +++ b/Swift/Controllers/UIInterfaces/MUCSearchWindow.h @@ -26,6 +26,7 @@ namespace Swift { virtual void clearList() = 0; virtual void addService(const MUCService& service) = 0; virtual void addSavedServices(const std::vector<JID>& services) = 0; + virtual void setSearchInProgress(bool searching) = 0; virtual void show() = 0; diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h index 4e15b27..acb7638 100644 --- a/Swift/Controllers/UIInterfaces/UIFactory.h +++ b/Swift/Controllers/UIInterfaces/UIFactory.h @@ -12,10 +12,11 @@ #include <Swift/Controllers/UIInterfaces/LoginWindowFactory.h> #include <Swift/Controllers/UIInterfaces/MainWindowFactory.h> #include <Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> #include <Swift/Controllers/UIInterfaces/XMLConsoleWidgetFactory.h> namespace Swift { - class UIFactory : public ChatListWindowFactory, public ChatWindowFactory, public EventWindowFactory, public LoginWindowFactory, public MainWindowFactory, public MUCSearchWindowFactory, public XMLConsoleWidgetFactory { + class UIFactory : public ChatListWindowFactory, public ChatWindowFactory, public EventWindowFactory, public LoginWindowFactory, public MainWindowFactory, public MUCSearchWindowFactory, public XMLConsoleWidgetFactory, public UserSearchWindowFactory { public: virtual ~UIFactory() {} }; diff --git a/Swift/Controllers/UIInterfaces/UserSearchWindow.h b/Swift/Controllers/UIInterfaces/UserSearchWindow.h new file mode 100644 index 0000000..dda3409 --- /dev/null +++ b/Swift/Controllers/UIInterfaces/UserSearchWindow.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include "Swiften/Base/boost_bsignals.h" + +#include <vector> + +#include "Swiften/Base/String.h" +#include "Swiften/JID/JID.h" +#include "Swift/Controllers/Chat/UserSearchController.h" + +namespace Swift { + + class UserSearchWindow { + public: + virtual ~UserSearchWindow() {}; + + virtual void clear() = 0; + virtual void setResults(const std::vector<UserSearchResult>& results) = 0; + virtual void addSavedServices(const std::vector<JID>& services) = 0; + virtual void setSelectedService(const JID& service) = 0; + virtual void setServerSupportsSearch(bool support) = 0; + virtual void setSearchError(bool support) = 0; + virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields) = 0; + virtual void show() = 0; + + boost::signal<void (const JID&)> onFormRequested; + boost::signal<void (boost::shared_ptr<SearchPayload>, const JID&)> onSearchRequested; + }; +} diff --git a/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h b/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h new file mode 100644 index 0000000..808c1db --- /dev/null +++ b/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include "Swift/Controllers/UIInterfaces/UserSearchWindow.h" + +namespace Swift { + class UIEventStream; + class UserSearchWindowFactory { + public: + virtual ~UserSearchWindowFactory() {}; + + virtual UserSearchWindow* createUserSearchWindow(UIEventStream* eventStream) = 0; + }; +} diff --git a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp index c31230c..7d2caba 100644 --- a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp +++ b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp @@ -7,6 +7,9 @@ #include "Swift/QtUI/MUCSearch/QtMUCSearchWindow.h" #include <qdebug.h> +#include <QMovie> +#include <QScrollBar> +#include <QTimer> #include "Swift/Controllers/UIEvents/UIEventStream.h" #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h" @@ -41,12 +44,41 @@ QtMUCSearchWindow::QtMUCSearchWindow(UIEventStream* eventStream) { connect(joinButton_, SIGNAL(clicked()), this, SLOT(handleJoin())); connect(results_, SIGNAL(clicked(const QModelIndex&)), this, SLOT(handleSelected(const QModelIndex&))); connect(results_, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleActivated(const QModelIndex&))); + throbber_ = new QLabel("Searching", results_); + throbber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), throbber_)); + throbber_->setToolTip("Searching"); + hasHadScrollBars_ = false; + updateThrobberPosition(); + setSearchInProgress(false); } QtMUCSearchWindow::~QtMUCSearchWindow() { } +void QtMUCSearchWindow::resizeEvent(QResizeEvent* /*event*/) { + updateThrobberPosition(); +} + +void QtMUCSearchWindow::updateThrobberPosition() { + bool isShown = throbber_->isVisible(); + int resultWidth = results_->width(); + int resultHeight = results_->height(); + //throbberWidth = throbber_->movie()->scaledSize().width(); + //throbberHeight = throbber_->movie()->scaledSize().height(); + int throbberWidth = 16; /* This is nasty, but the above doesn't work! */ + int throbberHeight = 16; + /* It's difficult (or I spent a while trying) to work out whether the scrollbars are currently shown and their appropriate size, + * because if you listen for the expanded/collapsed signals, you seem to get them before the scrollbars are updated. + * This seems an acceptable workaround. + */ + hasHadScrollBars_ |= results_->verticalScrollBar()->isVisible(); + int hMargin = hasHadScrollBars_ ? results_->verticalScrollBar()->width() + 2 : 2; + int vMargin = 2; /* We don't get horizontal scrollbars */ + throbber_->setGeometry(QRect(resultWidth - throbberWidth - hMargin, resultHeight - throbberHeight - vMargin, throbberWidth, throbberHeight)); /* include margins */ + throbber_->setVisible(isShown); +} + void QtMUCSearchWindow::addSavedServices(const std::vector<JID>& services) { service_->clear(); foreach (JID jid, services) { @@ -145,6 +177,7 @@ void QtMUCSearchWindow::clearList() { } void QtMUCSearchWindow::addService(const MUCService& service) { + updateThrobberPosition(); MUCSearchServiceItem* serviceItem = new MUCSearchServiceItem(P2QSTRING(service.getJID().toString())); foreach (MUCService::MUCRoom room, service.getRooms()) { new MUCSearchRoomItem(P2QSTRING(room.getNode()), serviceItem); @@ -153,4 +186,13 @@ void QtMUCSearchWindow::addService(const MUCService& service) { results_->expandAll(); } +void QtMUCSearchWindow::setSearchInProgress(bool searching) { + if (searching) { + throbber_->movie()->start(); + } else { + throbber_->movie()->stop(); + } + throbber_->setVisible(searching); +} + } diff --git a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h index 27ccdcb..b8cf953 100644 --- a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h +++ b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h @@ -25,19 +25,25 @@ namespace Swift { virtual void clearList(); virtual void addService(const MUCService& service); virtual void addSavedServices(const std::vector<JID>& services); + virtual void setSearchInProgress(bool searching); virtual void show(); + protected: + virtual void resizeEvent(QResizeEvent* event); private slots: void handleSearch(const QString& text); void handleSearch(); void handleJoin(); void handleSelected(const QModelIndex& current); void handleActivated(const QModelIndex& index); + void updateThrobberPosition(); private: void createAutoJoin(const JID& room, boost::optional<String> passedNick); MUCSearchModel* model_; MUCSearchDelegate* delegate_; UIEventStream* eventStream_; + QLabel* throbber_; String lastSetNick_; + bool hasHadScrollBars_; }; } diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index ee83f4d..6e53258 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -24,7 +24,7 @@ #include "QtSwiftUtil.h" #include "QtTabWidget.h" #include "Roster/QtTreeWidget.h" -#include "Swift/Controllers/UIEvents/AddContactUIEvent.h" +#include "Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h" #include "Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h" #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h" #include "Swift/Controllers/UIEvents/ToggleShowOfflineUIEvent.h" @@ -84,9 +84,9 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS QAction* joinMUCAction = new QAction("Join Room", this); connect(joinMUCAction, SIGNAL(triggered()), SLOT(handleJoinMUCAction())); actionsMenu->addAction(joinMUCAction); - addAction_ = new QAction("Add Contact", this); - connect(addAction_, SIGNAL(triggered(bool)), this, SLOT(handleAddActionTriggered(bool))); - actionsMenu->addAction(addAction_); + otherUserAction_ = new QAction("Find Other Contact", this); + connect(otherUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleOtherUserActionTriggered(bool))); + actionsMenu->addAction(otherUserAction_); QAction* signOutAction = new QAction("Sign Out", this); connect(signOutAction, SIGNAL(triggered()), SLOT(handleSignOutAction())); actionsMenu->addAction(signOutAction); @@ -123,22 +123,15 @@ void QtMainWindow::handleEventCountUpdated(int count) { tabs_->setTabText(eventIndex, text); } -void QtMainWindow::handleAddActionTriggered(bool checked) { - Q_UNUSED(checked); - QtAddContactDialog* addContact = new QtAddContactDialog(this); - connect(addContact, SIGNAL(onAddCommand(const JID&, const QString&)), SLOT(handleAddContactDialogComplete(const JID&, const QString&))); - addContact->show(); +void QtMainWindow::handleOtherUserActionTriggered(bool /*checked*/) { + boost::shared_ptr<UIEvent> event(new RequestUserSearchUIEvent()); + uiEventStream_->send(event); } void QtMainWindow::handleSignOutAction() { onSignOutRequest(); } -void QtMainWindow::handleAddContactDialogComplete(const JID& contact, const QString& name) { - boost::shared_ptr<UIEvent> event(new AddContactUIEvent(contact, Q2PSTRING(name))); - uiEventStream_->send(event); -} - void QtMainWindow::handleJoinMUCAction() { uiEventStream_->send(boost::shared_ptr<UIEvent>(new RequestMUCSearchUIEvent())); } diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h index f494fd0..10cad66 100644 --- a/Swift/QtUI/QtMainWindow.h +++ b/Swift/QtUI/QtMainWindow.h @@ -54,15 +54,14 @@ namespace Swift { void handleShowOfflineToggled(bool); void handleJoinMUCAction(); void handleSignOutAction(); - void handleAddContactDialogComplete(const JID& contact, const QString& name); - void handleAddActionTriggered(bool checked); + void handleOtherUserActionTriggered(bool checked); void handleEventCountUpdated(int count); private: std::vector<QMenu*> menus_; QtTreeWidget* treeWidget_; QtRosterHeader* meView_; - QAction* addAction_; + QAction* otherUserAction_; QAction* showOfflineAction_; QtTabWidget* tabs_; QWidget* contactsTabWidget_; diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h index 32e261c..ca6e126 100644 --- a/Swift/QtUI/QtSwift.h +++ b/Swift/QtUI/QtSwift.h @@ -42,6 +42,7 @@ namespace Swift { class QtChatWindowFactory; class QtSoundPlayer; class QtMUCSearchWindowFactory; + class QtUserSearchWindowFactory; class EventLoop; class QtSwift : public QObject { diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 55ed370..4f1bef5 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -19,6 +19,7 @@ #include "QtChatWindowFactory.h" #include "QtSwiftUtil.h" #include "MUCSearch/QtMUCSearchWindow.h" +#include "UserSearch/QtUserSearchWindow.h" namespace Swift { @@ -77,4 +78,8 @@ ChatWindow* QtUIFactory::createChatWindow(const JID& contact, UIEventStream* eve return chatWindowFactory->createChatWindow(contact, eventStream); } +UserSearchWindow* QtUIFactory::createUserSearchWindow(UIEventStream* eventStream) { + return new QtUserSearchWindow(eventStream); +}; + } diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h index 7f610a2..5282b31 100644 --- a/Swift/QtUI/QtUIFactory.h +++ b/Swift/QtUI/QtUIFactory.h @@ -33,6 +33,7 @@ namespace Swift { virtual ChatListWindow* createChatListWindow(UIEventStream*); virtual MUCSearchWindow* createMUCSearchWindow(UIEventStream* eventStream); virtual ChatWindow* createChatWindow(const JID &contact, UIEventStream* eventStream); + virtual UserSearchWindow* createUserSearchWindow(UIEventStream* eventStream); private slots: void handleLoginWindowGeometryChanged(); diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index b435e34..0f08556 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -111,6 +111,9 @@ sources = [ "MUCSearch/MUCSearchModel.cpp", "MUCSearch/MUCSearchRoomItem.cpp", "MUCSearch/MUCSearchDelegate.cpp", + "UserSearch/QtUserSearchWindow.cpp", + "UserSearch/UserSearchModel.cpp", + "UserSearch/UserSearchDelegate.cpp", "ContextMenus/QtRosterContextMenu.cpp", "ContextMenus/QtContextMenu.cpp", "QtSubscriptionRequestWindow.cpp", @@ -138,6 +141,7 @@ else : swiftProgram = myenv.Program("swift", sources) myenv.Uic4("MUCSearch/QtMUCSearchWindow.ui") +myenv.Uic4("UserSearch/QtUserSearchWindow.ui") myenv.Uic4("QtAddContactDialog.ui") myenv.Uic4("QtBookmarkDetailWindow.ui") myenv.Qrc("DefaultTheme.qrc") diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp new file mode 100644 index 0000000..c3038d8 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swift/QtUI/UserSearch/QtUserSearchWindow.h" + +#include <qdebug.h> +#include <QModelIndex> + +#include "Swift/Controllers/UIEvents/UIEventStream.h" +#include "Swift/Controllers/UIEvents/RequestChatUIEvent.h" +#include "Swift/Controllers/UIEvents/AddContactUIEvent.h" +#include "Swift/QtUI/UserSearch/UserSearchModel.h" +#include "Swift/QtUI/UserSearch/UserSearchDelegate.h" +#include "Swift/QtUI/QtSwiftUtil.h" + +namespace Swift { + +QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream) { +#ifndef Q_WS_MAC + setWindowIcon(QIcon(":/logo-icon-16.png")); +#endif + eventStream_ = eventStream; + setupUi(this); + model_ = new UserSearchModel(); + delegate_ = new UserSearchDelegate(); + results_->setModel(model_); + results_->setItemDelegate(delegate_); + results_->setHeaderHidden(true); +#ifdef SWIFT_PLATFORM_MACOSX + results_->setAlternatingRowColors(true); +#endif + connect(service_, SIGNAL(activated(const QString&)), this, SLOT(handleGetForm())); + connect(getSearchForm_, SIGNAL(clicked()), this, SLOT(handleGetForm())); + //connect(user_, SIGNAL(returnPressed()), this, SLOT(handleJoin())); + //connect(nickName_, SIGNAL(returnPressed()), room_, SLOT(setFocus())); + connect(search_, SIGNAL(clicked()), this, SLOT(handleSearch())); + + connect(results_, SIGNAL(clicked(const QModelIndex&)), this, SLOT(handleSelected(const QModelIndex&))); + connect(results_, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleActivated(const QModelIndex&))); + connect(buttonBox_, SIGNAL(accepted()), this, SLOT(handleOkClicked())); + connect(buttonBox_, SIGNAL(rejected()), this, SLOT(handleCancelClicked())); + /* When inputs change, check if OK is enabled */ + connect(jid_, SIGNAL(textChanged(const QString&)), this, SLOT(enableCorrectButtons())); + connect(nickName_, SIGNAL(textChanged(const QString&)), this, SLOT(enableCorrectButtons())); + connect(startChat_, SIGNAL(stateChanged(int)), this, SLOT(enableCorrectButtons())); + connect(addToRoster_, SIGNAL(stateChanged(int)), this, SLOT(enableCorrectButtons())); + enableCorrectButtons(); +} + +QtUserSearchWindow::~QtUserSearchWindow() { + +} + +void QtUserSearchWindow::handleGetForm() { + lastServiceJID_ = JID(Q2PSTRING(service_->currentText())); + onFormRequested(lastServiceJID_); +} + +void QtUserSearchWindow::handleSearch() { + boost::shared_ptr<SearchPayload> search(new SearchPayload()); + if (nickInput_->isEnabled()) { + search->setNick(Q2PSTRING(nickInput_->text())); + } + if (firstInput_->isEnabled()) { + search->setFirst(Q2PSTRING(firstInput_->text())); + } + if (lastInput_->isEnabled()) { + search->setLast(Q2PSTRING(lastInput_->text())); + } + if (emailInput_->isEnabled()) { + search->setEMail(Q2PSTRING(emailInput_->text())); + } + onSearchRequested(search, lastServiceJID_); +} + +void QtUserSearchWindow::show() { + clear(); + enableCorrectButtons(); + QWidget::show(); +} + +void QtUserSearchWindow::enableCorrectButtons() { + bool enable = !jid_->text().isEmpty() && (startChat_->isChecked() || (addToRoster_->isChecked() && !nickName_->text().isEmpty())); + buttonBox_->button(QDialogButtonBox::Ok)->setEnabled(enable); +} + +void QtUserSearchWindow::handleOkClicked() { + JID contact = JID(Q2PSTRING(jid_->text())); + String nick = Q2PSTRING(nickName_->text()); + if (addToRoster_->isChecked()) { + boost::shared_ptr<UIEvent> event(new AddContactUIEvent(contact, nick)); + eventStream_->send(event); + } + if (startChat_->isChecked()) { + boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(contact)); + eventStream_->send(event); + } + hide(); +} + +void QtUserSearchWindow::handleCancelClicked() { + hide(); +} + +void QtUserSearchWindow::addSavedServices(const std::vector<JID>& services) { + service_->clear(); + foreach (JID jid, services) { + service_->addItem(P2QSTRING(jid.toString())); + } + service_->clearEditText(); +} + +void QtUserSearchWindow::setServerSupportsSearch(bool support) { + if (!support) { + stack_->setCurrentIndex(0); + messageLabel_->setText("This service doesn't support searching for users."); + search_->setEnabled(false); + } +} + +void QtUserSearchWindow::setSearchFields(boost::shared_ptr<SearchPayload> fields) { + bool enabled[8] = {fields->getNick(), fields->getNick(), fields->getFirst(), fields->getFirst(), fields->getLast(), fields->getLast(), fields->getEMail(), fields->getEMail()}; + QWidget* widget[8] = {nickInputLabel_, nickInput_, firstInputLabel_, firstInput_, lastInputLabel_, lastInput_, emailInputLabel_, emailInput_}; + for (int i = 0; i < 8; i++) { + widget[i]->setVisible(enabled[i]); + widget[i]->setEnabled(enabled[i]); + } + stack_->setCurrentIndex(1); + search_->setEnabled(true); +} + +void QtUserSearchWindow::handleActivated(const QModelIndex& index) { + if (!index.isValid()) { + return; + } + UserSearchResult* userItem = static_cast<UserSearchResult*>(index.internalPointer()); + if (userItem) { /* static cast, so always will be, but if we change to be like mucsearch, remember the check.*/ + handleSelected(index); + //handleJoin(); /* Don't do anything automatically on selection.*/ + } +} + +void QtUserSearchWindow::handleSelected(const QModelIndex& current) { + if (!current.isValid()) { + return; + } + UserSearchResult* userItem = static_cast<UserSearchResult*>(current.internalPointer()); + if (userItem) { /* Remember to leave this if we change to dynamic cast */ + jid_->setText(P2QSTRING(userItem->getJID().toString())); + } +} + +void QtUserSearchWindow::setResults(const std::vector<UserSearchResult>& results) { + model_->setResults(results); +} + +void QtUserSearchWindow::setSelectedService(const JID& jid) { + service_->setEditText(P2QSTRING(jid.toString())); +} + +void QtUserSearchWindow::clear() { + stack_->setCurrentIndex(0); + messageLabel_->setText("Please choose a service to search, above"); + model_->clear(); + search_->setEnabled(false); +} + +void QtUserSearchWindow::setSearchError(bool error) { + //FIXME +} + +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h new file mode 100644 index 0000000..dca321a --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include "Swift/QtUI/UserSearch/ui_QtUserSearchWindow.h" + +#include "Swift/Controllers/UIInterfaces/UserSearchWindow.h" + + +namespace Swift { + class UserSearchModel; + class UserSearchDelegate; + class UserSearchResult; + class UIEventStream; + class QtUserSearchWindow : public QWidget, public UserSearchWindow, private Ui::QtUserSearchWindow { + Q_OBJECT + public: + QtUserSearchWindow(UIEventStream* eventStream); + virtual ~QtUserSearchWindow(); + + virtual void addSavedServices(const std::vector<JID>& services); + + virtual void clear(); + virtual void show(); + virtual void setResults(const std::vector<UserSearchResult>& results); + virtual void setSelectedService(const JID& jid); + virtual void setServerSupportsSearch(bool error); + virtual void setSearchError(bool error); + virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields) ; + private slots: + void handleGetForm(); + void handleSelected(const QModelIndex& current); + void handleSearch(); + void handleActivated(const QModelIndex& index); + void handleOkClicked(); + void handleCancelClicked(); + void enableCorrectButtons(); + private: + UserSearchModel* model_; + UserSearchDelegate* delegate_; + UIEventStream* eventStream_; + JID lastServiceJID_; + }; +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.ui b/Swift/QtUI/UserSearch/QtUserSearchWindow.ui new file mode 100644 index 0000000..56047ce --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.ui @@ -0,0 +1,292 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtUserSearchWindow</class> + <widget class="QWidget" name="QtUserSearchWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>698</width> + <height>569</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Find other users</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetNoConstraint</enum> + </property> + <item> + <widget class="QFrame" name="frame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Service to search:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="service_"> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="getSearchForm_"> + <property name="text"> + <string>Get Search Form</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QStackedWidget" name="stack_"> + <property name="currentIndex"> + <number>1</number> + </property> + <widget class="QWidget" name="display"> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <item> + <widget class="QLabel" name="messageLabel_"> + <property name="text"> + <string>TextLabel</string> + </property> + <property name="alignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="legacy"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Enter search terms</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="nickInputLabel_"> + <property name="text"> + <string>Nickname:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="nickInput_"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="firstInputLabel_"> + <property name="text"> + <string>First name:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="firstInput_"/> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="lastInputLabel_"> + <property name="text"> + <string>Last name:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLineEdit" name="lastInput_"/> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="emailInputLabel_"> + <property name="text"> + <string>E-Mail:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLineEdit" name="emailInput_"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="search_"> + <property name="text"> + <string>Search</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QFrame" name="frame_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Results:</string> + </property> + </widget> + </item> + <item> + <widget class="QTreeView" name="results_"/> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Address:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="jid_"/> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QCheckBox" name="addToRoster_"> + <property name="text"> + <string>Add to Roster. Nickname:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="nickName_"/> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QCheckBox" name="startChat_"> + <property name="text"> + <string>Start Chat With Contact</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox_"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/Swift/QtUI/UserSearch/UserSearchDelegate.cpp b/Swift/QtUI/UserSearch/UserSearchDelegate.cpp new file mode 100644 index 0000000..ff3e766 --- /dev/null +++ b/Swift/QtUI/UserSearch/UserSearchDelegate.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <QPen> +#include <QPainter> + +#include "Swift/QtUI/UserSearch/UserSearchDelegate.h" +//#include "Swift/QtUI/Roster/GroupItemDelegate.h" +//#include "Swift/QtUI/MUCSearch/MUCSearchItem.h" +//#include "Swift/QtUI/MUCSearch/MUCSearchRoomItem.h" +//#include "Swift/QtUI/MUCSearch/MUCSearchServiceItem.h" + +namespace Swift { + +UserSearchDelegate::UserSearchDelegate() { + +} + +UserSearchDelegate::~UserSearchDelegate() { + +} + +// QSize MUCSearchDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index ) const { +// // MUCSearchItem* item = static_cast<MUCSearchItem*>(index.internalPointer()); +// // if (item && dynamic_cast<MUCSearchMUCItem*>(item)) { +// // return mucSizeHint(option, index); +// // } else if (item && dynamic_cast<MUCSearchGroupItem*>(item)) { +// // return groupDelegate_->sizeHint(option, index); +// // } +// return QStyledItemDelegate::sizeHint(option, index); +// } + +// QSize MUCSearchDelegate::mucSizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/ ) const { +// QFontMetrics nameMetrics(common_.nameFont); +// QFontMetrics statusMetrics(common_.detailFont); +// int sizeByText = 2 * common_.verticalMargin + nameMetrics.height() + statusMetrics.height(); +// return QSize(150, sizeByText); +// } + +// void MUCSearchDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { +// MUCSearchItem* item = static_cast<MUCSearchItem*>(index.internalPointer()); +// if (item && dynamic_cast<MUCSearchMUCItem*>(item)) { +// paintMUC(painter, option, dynamic_cast<MUCSearchMUCItem*>(item)); +// } else if (item && dynamic_cast<MUCSearchGroupItem*>(item)) { +// MUCSearchGroupItem* group = dynamic_cast<MUCSearchGroupItem*>(item); +// groupDelegate_->paint(painter, option, group->data(Qt::DisplayRole).toString(), group->rowCount(), option.state & QStyle::State_Open); +// } else { +// QStyledItemDelegate::paint(painter, option, index); +// } +// } + +// void MUCSearchDelegate::paintMUC(QPainter* painter, const QStyleOptionViewItem& option, MUCSearchMUCItem* item) const { +// painter->save(); +// QRect fullRegion(option.rect); +// if ( option.state & QStyle::State_Selected ) { +// painter->fillRect(fullRegion, option.palette.highlight()); +// painter->setPen(option.palette.highlightedText().color()); +// } else { +// QColor nameColor = item->data(Qt::TextColorRole).value<QColor>(); +// painter->setPen(QPen(nameColor)); +// } + +// QFontMetrics nameMetrics(common_.nameFont); +// painter->setFont(common_.nameFont); +// int extraFontWidth = nameMetrics.width("H"); +// int leftOffset = common_.horizontalMargin * 2 + extraFontWidth / 2; +// QRect textRegion(fullRegion.adjusted(leftOffset, 0, 0, 0)); + +// int nameHeight = nameMetrics.height() + common_.verticalMargin; +// QRect nameRegion(textRegion.adjusted(0, common_.verticalMargin, 0, 0)); + +// painter->drawText(nameRegion, Qt::AlignTop, item->data(Qt::DisplayRole).toString()); + +// painter->setFont(common_.detailFont); +// painter->setPen(QPen(QColor(160,160,160))); + +// QRect detailRegion(textRegion.adjusted(0, nameHeight, 0, 0)); +// painter->drawText(detailRegion, Qt::AlignTop, item->data(DetailTextRole).toString()); + +// painter->restore(); +// } + +} diff --git a/Swift/QtUI/UserSearch/UserSearchDelegate.h b/Swift/QtUI/UserSearch/UserSearchDelegate.h new file mode 100644 index 0000000..d046d62 --- /dev/null +++ b/Swift/QtUI/UserSearch/UserSearchDelegate.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +#include "Swift/QtUI/Roster/DelegateCommons.h" + +namespace Swift { + class UserSearchDelegate : public QStyledItemDelegate { + public: + UserSearchDelegate(); + ~UserSearchDelegate(); + }; + +} + diff --git a/Swift/QtUI/UserSearch/UserSearchModel.cpp b/Swift/QtUI/UserSearch/UserSearchModel.cpp new file mode 100644 index 0000000..782d2d0 --- /dev/null +++ b/Swift/QtUI/UserSearch/UserSearchModel.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swift/QtUI/UserSearch/UserSearchModel.h" + +#include "Swift/QtUI/QtSwiftUtil.h" + +namespace Swift { + +UserSearchModel::UserSearchModel() { +} + +void UserSearchModel::clear() { + emit layoutAboutToBeChanged(); + results_.clear(); + emit layoutChanged(); +} + +void UserSearchModel::setResults(const std::vector<UserSearchResult>& results) { + clear(); + emit layoutAboutToBeChanged(); + results_ = results; + emit layoutChanged(); +} + +int UserSearchModel::columnCount(const QModelIndex& /*parent*/) const { + return 1; +} + +QVariant UserSearchModel::data(const QModelIndex& index, int role) const { + if (!index.isValid()) return QVariant(); + UserSearchResult* result = static_cast<UserSearchResult*>(index.internalPointer()); + switch (role) { + case Qt::DisplayRole: return QVariant(P2QSTRING(result->getJID().toString())); + default: return QVariant(); + } +} + +QModelIndex UserSearchModel::index(int row, int column, const QModelIndex & parent) const { + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + return row < (int)results_.size() ? createIndex(row, column, (void*)&(results_[row])) : QModelIndex(); +} + +QModelIndex UserSearchModel::parent(const QModelIndex& /*index*/) const { + return QModelIndex(); +} + +int UserSearchModel::rowCount(const QModelIndex& parentIndex) const { + if (!parentIndex.isValid()) { + return results_.size(); + } + return 0; +} + +} diff --git a/Swift/QtUI/UserSearch/UserSearchModel.h b/Swift/QtUI/UserSearch/UserSearchModel.h new file mode 100644 index 0000000..d766d9a --- /dev/null +++ b/Swift/QtUI/UserSearch/UserSearchModel.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <boost/shared_ptr.hpp> + +#include <QAbstractItemModel> +#include <QList> + +#include "Swift/Controllers/Chat/UserSearchController.h" + +namespace Swift { + class UserSearchModel : public QAbstractItemModel { + Q_OBJECT + public: + UserSearchModel(); + void clear(); + void setResults(const std::vector<UserSearchResult>& results); + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + private: + std::vector<UserSearchResult> results_; + }; + +} diff --git a/Swiften/Elements/DiscoInfo.cpp b/Swiften/Elements/DiscoInfo.cpp index 276b341..a939d48 100644 --- a/Swiften/Elements/DiscoInfo.cpp +++ b/Swiften/Elements/DiscoInfo.cpp @@ -10,6 +10,7 @@ namespace Swift { const String DiscoInfo::ChatStatesFeature = String("http://jabber.org/protocol/chatstates"); const String DiscoInfo::SecurityLabelsFeature = String("urn:xmpp:sec-label:0"); +const String DiscoInfo::JabberSearchFeature = String("jabber:iq:search"); bool DiscoInfo::Identity::operator<(const Identity& other) const { diff --git a/Swiften/Elements/DiscoInfo.h b/Swiften/Elements/DiscoInfo.h index 038a2f1..41bf6bf 100644 --- a/Swiften/Elements/DiscoInfo.h +++ b/Swiften/Elements/DiscoInfo.h @@ -21,6 +21,7 @@ namespace Swift { static const String ChatStatesFeature; static const String SecurityLabelsFeature; + static const String JabberSearchFeature; const static std::string SecurityLabels; class Identity { diff --git a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp index f57411b..1bbcbf2 100644 --- a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp +++ b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp @@ -39,6 +39,7 @@ #include "Swiften/Serializer/PayloadSerializers/CommandSerializer.h" #include "Swiften/Serializer/PayloadSerializers/InBandRegistrationPayloadSerializer.h" #include "Swiften/Serializer/PayloadSerializers/NicknameSerializer.h" +#include "Swiften/Serializer/PayloadSerializers/SearchPayloadSerializer.h" namespace Swift { @@ -75,6 +76,7 @@ FullPayloadSerializerCollection::FullPayloadSerializerCollection() { serializers_.push_back(new CommandSerializer()); serializers_.push_back(new InBandRegistrationPayloadSerializer()); serializers_.push_back(new NicknameSerializer()); + serializers_.push_back(new SearchPayloadSerializer()); foreach(PayloadSerializer* serializer, serializers_) { addSerializer(serializer); } |