diff options
Diffstat (limited to 'Swift/QtUI')
32 files changed, 1485 insertions, 299 deletions
diff --git a/Swift/QtUI/ChatList/ChatListModel.h b/Swift/QtUI/ChatList/ChatListModel.h index e384a04..04e369a 100644 --- a/Swift/QtUI/ChatList/ChatListModel.h +++ b/Swift/QtUI/ChatList/ChatListModel.h @@ -6,8 +6,6 @@ #pragma once -#include <boost/shared_ptr.hpp> - #include <QAbstractItemModel> #include <QList> diff --git a/Swift/QtUI/ChatList/ChatListRecentItem.cpp b/Swift/QtUI/ChatList/ChatListRecentItem.cpp index 5a4d1e1..e9ecec8 100644 --- a/Swift/QtUI/ChatList/ChatListRecentItem.cpp +++ b/Swift/QtUI/ChatList/ChatListRecentItem.cpp @@ -20,7 +20,7 @@ const ChatListWindow::Chat& ChatListRecentItem::getChat() const { QVariant ChatListRecentItem::data(int role) const { switch (role) { - case Qt::DisplayRole: return P2QSTRING(chat_.chatName); + case Qt::DisplayRole: return chat_.impromptuJIDs.empty() ? P2QSTRING(chat_.chatName) : P2QSTRING(chat_.getImpromptuTitle()); case DetailTextRole: return P2QSTRING(chat_.activity); /*case Qt::TextColorRole: return textColor_; case Qt::BackgroundColorRole: return backgroundColor_; diff --git a/Swift/QtUI/ChatList/QtChatListWindow.cpp b/Swift/QtUI/ChatList/QtChatListWindow.cpp index 9692c9c..4d1f19b 100644 --- a/Swift/QtUI/ChatList/QtChatListWindow.cpp +++ b/Swift/QtUI/ChatList/QtChatListWindow.cpp @@ -30,7 +30,7 @@ namespace Swift { QtChatListWindow::QtChatListWindow(UIEventStream *uiEventStream, SettingsProvider* settings, QWidget* parent) : QTreeView(parent) { eventStream_ = uiEventStream; - settings_ = settings;; + settings_ = settings; bookmarksEnabled_ = false; model_ = new ChatListModel(); setModel(model_); diff --git a/Swift/QtUI/QtChatTabs.cpp b/Swift/QtUI/QtChatTabs.cpp index a119043..de1ee7c 100644 --- a/Swift/QtUI/QtChatTabs.cpp +++ b/Swift/QtUI/QtChatTabs.cpp @@ -181,10 +181,9 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { } QString tabText = tabbable->windowTitle().simplified(); - // look for spectrum-generated and other long JID localparts, and // try to abbreviate the resulting long tab texts - QRegExp hasTrailingGarbage("^(.[-\\w\\s&]+)([^\\s\\w].*)$"); + QRegExp hasTrailingGarbage("^(.[-\\w\\s,&]+)([^\\s\\,w].*)$"); if (hasTrailingGarbage.exactMatch(tabText) && hasTrailingGarbage.cap(1).simplified().length() >= 2 && hasTrailingGarbage.cap(2).length() >= 7) { @@ -193,10 +192,8 @@ void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { // least a couple of characters. tabText = hasTrailingGarbage.cap(1).simplified(); } - // QTabBar interprets &, so escape that tabText.replace("&", "&&"); - // see which alt[a-z] keys other tabs use bool accelsTaken[26]; int i = 0; diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index d81de61..2dfef5a 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -16,7 +16,6 @@ #include "QtTextEdit.h" #include "QtSettingsProvider.h" #include "QtScaledAvatarCache.h" -#include "QtInviteToChatWindow.h" #include <Swift/QtUI/QtUISettingConstants.h> #include <Swiften/StringCodecs/Base64.h> @@ -68,7 +67,7 @@ const QString QtChatWindow::ButtonFileTransferAcceptRequest = QString("filetrans const QString QtChatWindow::ButtonMUCInvite = QString("mucinvite"); -QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageKind_(PreviosuMessageWasNone), eventStream_(eventStream), blockingState_(BlockingUnsupported) { +QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventStream* eventStream, SettingsProvider* settings) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageKind_(PreviosuMessageWasNone), eventStream_(eventStream), blockingState_(BlockingUnsupported), isMUC_(false), supportsImpromptuChat_(false) { settings_ = settings; unreadCount_ = 0; idCounter_ = 0; @@ -189,11 +188,10 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt jsBridge = new QtChatWindowJSBridge(); messageLog_->addToJSEnvironment("chatwindow", jsBridge); - connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString))); + connect(jsBridge, SIGNAL(buttonClicked(QString,QString,QString,QString,QString,QString)), this, SLOT(handleHTMLButtonClicked(QString,QString,QString,QString,QString,QString))); settings_->onSettingChanged.connect(boost::bind(&QtChatWindow::handleSettingChanged, this, _1)); showEmoticons_ = settings_->getSetting(QtUISettingConstants::SHOW_EMOTICONS); - } QtChatWindow::~QtChatWindow() { @@ -425,10 +423,11 @@ void QtChatWindow::closeEvent(QCloseEvent* event) { onClosed(); } -void QtChatWindow::convertToMUC() { - setAcceptDrops(false); +void QtChatWindow::convertToMUC(bool impromptuMUC) { + impromptu_ = impromptuMUC; + isMUC_ = true; treeWidget_->show(); - subject_->show(); + subject_->setVisible(!impromptu_); } void QtChatWindow::qAppFocusChanged(QWidget* /*old*/, QWidget* /*now*/) { @@ -680,10 +679,10 @@ static QString decodeButtonArgument(const QString& str) { return P2QSTRING(byteArrayToString(Base64::decode(Q2PSTRING(str)))); } -QString QtChatWindow::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3) { +QString QtChatWindow::buildChatWindowButton(const QString& name, const QString& id, const QString& arg1, const QString& arg2, const QString& arg3, const QString& arg4, const QString& arg5) { QRegExp regex("[A-Za-z][A-Za-z0-9\\-\\_]+"); Q_ASSERT(regex.exactMatch(id)); - QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)); + QString html = QString("<input id='%2' type='submit' value='%1' onclick='chatwindow.buttonClicked(\"%2\", \"%3\", \"%4\", \"%5\", \"%6\", \"%7\");' />").arg(name).arg(id).arg(encodeButtonArgument(arg1)).arg(encodeButtonArgument(arg2)).arg(encodeButtonArgument(arg3)).arg(encodeButtonArgument(arg4)).arg(encodeButtonArgument(arg5)); return html; } @@ -776,10 +775,12 @@ void QtChatWindow::setWhiteboardSessionStatus(std::string id, const ChatWindow:: messageLog_->setWhiteboardSessionStatus(P2QSTRING(id), state); } -void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString encodedArgument2, QString encodedArgument3) { +void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString encodedArgument2, QString encodedArgument3, QString encodedArgument4, QString encodedArgument5) { QString arg1 = decodeButtonArgument(encodedArgument1); QString arg2 = decodeButtonArgument(encodedArgument2); QString arg3 = decodeButtonArgument(encodedArgument3); + QString arg4 = decodeButtonArgument(encodedArgument4); + QString arg5 = decodeButtonArgument(encodedArgument5); if (id.startsWith(ButtonFileTransferCancel)) { QString ft_id = arg1; @@ -826,8 +827,9 @@ void QtChatWindow::handleHTMLButtonClicked(QString id, QString encodedArgument1, QString roomJID = arg1; QString password = arg2; QString elementID = arg3; - - eventStream_->send(boost::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password))); + QString isImpromptu = arg4; + QString isContinuation = arg5; + eventStream_->send(boost::make_shared<JoinMUCUIEvent>(Q2PSTRING(roomJID), Q2PSTRING(password), boost::optional<std::string>(), false, false, isImpromptu.contains("true"), isContinuation.contains("true"))); messageLog_->setMUCInvitationJoined(elementID); } else { @@ -957,18 +959,32 @@ void QtChatWindow::moveEvent(QMoveEvent*) { void QtChatWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() == 1) { // TODO: check whether contact actually supports file transfer - event->acceptProposedAction(); + if (!isMUC_) { + event->acceptProposedAction(); + } + } else if (event->mimeData()->hasFormat("application/vnd.swift.contact-jid")) { + if (isMUC_ || supportsImpromptuChat_) { + event->acceptProposedAction(); + } } } void QtChatWindow::dropEvent(QDropEvent *event) { - if (event->mimeData()->urls().size() == 1) { - onSendFileRequest(Q2PSTRING(event->mimeData()->urls().at(0).toLocalFile())); - } else { - std::string messageText(Q2PSTRING(tr("Sending of multiple files at once isn't supported at this time."))); - ChatMessage message; - message.append(boost::make_shared<ChatTextMessagePart>(messageText)); - addSystemMessage(message, DefaultDirection); + if (event->mimeData()->hasUrls()) { + if (event->mimeData()->urls().size() == 1) { + onSendFileRequest(Q2PSTRING(event->mimeData()->urls().at(0).toLocalFile())); + } else { + std::string messageText(Q2PSTRING(tr("Sending of multiple files at once isn't supported at this time."))); + ChatMessage message; + message.append(boost::make_shared<ChatTextMessagePart>(messageText)); + addSystemMessage(message, DefaultDirection); + } + } else if (event->mimeData()->hasFormat("application/vnd.swift.contact-jid")) { + QByteArray dataBytes = event->mimeData()->data("application/vnd.swift.contact-jid"); + QDataStream dataStream(&dataBytes, QIODevice::ReadOnly); + QString jidString; + dataStream >> jidString; + onInviteToChat(std::vector<JID>(1, JID(Q2PSTRING(jidString)))); } } @@ -1004,9 +1020,23 @@ void QtChatWindow::handleActionButtonClicked() { } else if (blockingState_ == IsUnblocked) { block = contextMenu.addAction(tr("Block")); } + + if (supportsImpromptuChat_) { + invite = contextMenu.addAction(tr("Invite person to this chat…")); + } + } else { foreach(ChatWindow::RoomAction availableAction, availableRoomActions_) { + if (impromptu_) { + // hide options we don't need in impromptu chats + if (availableAction == ChatWindow::ChangeSubject || + availableAction == ChatWindow::Configure || + availableAction == ChatWindow::Affiliations || + availableAction == ChatWindow::Destroy) { + continue; + } + } switch(availableAction) { case ChatWindow::ChangeSubject: changeSubject = contextMenu.addAction(tr("Change subject…")); break; @@ -1052,7 +1082,7 @@ void QtChatWindow::handleActionButtonClicked() { } } else if (result == invite) { - onInvitePersonToThisMUCRequest(); + onInviteToChat(std::vector<JID>()); } else if (result == block) { onBlockUserRequest(); @@ -1079,6 +1109,10 @@ void QtChatWindow::setBlockingState(BlockingState state) { blockingState_ = state; } +void QtChatWindow::setCanInitiateImpromptuChats(bool supportsImpromptu) { + supportsImpromptuChat_ = supportsImpromptu; +} + void QtChatWindow::showRoomConfigurationForm(Form::ref form) { if (mucConfigurationWindow_) { delete mucConfigurationWindow_.data(); @@ -1088,12 +1122,17 @@ void QtChatWindow::showRoomConfigurationForm(Form::ref form) { mucConfigurationWindow_->onFormCancelled.connect(boost::bind(boost::ref(onConfigurationFormCancelled))); } -void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct) { +void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct, bool isImpromptu, bool isContinuation) { if (isWidgetSelected()) { onAllMessagesRead(); } - QString message = QObject::tr("You've been invited to enter the %1 room.").arg(P2QSTRING(jid.toString())) + "\n"; + QString message; + if (isImpromptu) { + message = QObject::tr("You've been invited to join a chat.") + "\n"; + } else { + message = QObject::tr("You've been invited to enter the %1 room.").arg(P2QSTRING(jid.toString())) + "\n"; + } QString htmlString = message; if (!reason.empty()) { htmlString += QObject::tr("Reason: %1").arg(P2QSTRING(reason)) + "\n"; @@ -1106,7 +1145,7 @@ void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& ji QString id = QString(ButtonMUCInvite + "%1").arg(P2QSTRING(boost::lexical_cast<std::string>(idCounter_++))); htmlString += "<div id='" + id + "'>" + - buildChatWindowButton(chatMessageToHTML(ChatMessage(Q2PSTRING((tr("Accept Invite"))))), ButtonMUCInvite, QtUtilities::htmlEscape(P2QSTRING(jid.toString())), QtUtilities::htmlEscape(P2QSTRING(password)), id) + + buildChatWindowButton(chatMessageToHTML(ChatMessage(Q2PSTRING((tr("Accept Invite"))))), ButtonMUCInvite, QtUtilities::htmlEscape(P2QSTRING(jid.toString())), QtUtilities::htmlEscape(P2QSTRING(password)), id, QtUtilities::htmlEscape(isImpromptu ? "true" : "false"), QtUtilities::htmlEscape(isContinuation ? "true" : "false")) + "</div>"; bool appendToPrevious = appendToPreviousCheck(PreviousMessageWasMUCInvite, senderName, false); @@ -1124,10 +1163,4 @@ void QtChatWindow::addMUCInvitation(const std::string& senderName, const JID& ji previousMessageKind_ = PreviousMessageWasMUCInvite; } - -InviteToChatWindow* QtChatWindow::createInviteToChatWindow() { - return new QtInviteToChatWindow(this); -} - - } diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h index a29edad..ba16cfe 100644 --- a/Swift/QtUI/QtChatWindow.h +++ b/Swift/QtUI/QtChatWindow.h @@ -109,7 +109,7 @@ namespace Swift { void show(); void activate(); void setUnreadMessageCount(int count); - void convertToMUC(); + void convertToMUC(bool impromptuMUC = false); // TreeWidget *getTreeWidget(); void setAvailableSecurityLabels(const std::vector<SecurityLabelsCatalog::Item>& labels); void setSecurityLabelsEnabled(bool enabled); @@ -133,14 +133,13 @@ namespace Swift { virtual void setAvailableOccupantActions(const std::vector<OccupantAction>& actions); void setSubject(const std::string& subject); void showRoomConfigurationForm(Form::ref); - void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true); + void addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& reason, const std::string& password, bool direct = true, bool isImpromptu = false, bool isContinuation = false); void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&); void setAvailableRoomActions(const std::vector<RoomAction>& actions); void setBlockingState(BlockingState state); + virtual void setCanInitiateImpromptuChats(bool supportsImpromptu); - InviteToChatWindow* createInviteToChatWindow(); - - static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString()); + static QString buildChatWindowButton(const QString& name, const QString& id, const QString& arg1 = QString(), const QString& arg2 = QString(), const QString& arg3 = QString(), const QString& arg4 = QString(), const QString& arg5 = QString()); public slots: void handleChangeSplitterState(QByteArray state); @@ -176,7 +175,7 @@ namespace Swift { void handleAlertButtonClicked(); void handleActionButtonClicked(); - void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3); + void handleHTMLButtonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); void handleAffiliationEditorAccepted(); void handleCurrentLabelChanged(int); @@ -259,5 +258,8 @@ namespace Swift { QPalette defaultLabelsPalette_; LabelModel* labelModel_; BlockingState blockingState_; + bool impromptu_; + bool isMUC_; + bool supportsImpromptuChat_; }; } diff --git a/Swift/QtUI/QtChatWindowJSBridge.h b/Swift/QtUI/QtChatWindowJSBridge.h index 8e6f0c2..5a26302 100644 --- a/Swift/QtUI/QtChatWindowJSBridge.h +++ b/Swift/QtUI/QtChatWindowJSBridge.h @@ -20,7 +20,7 @@ public: QtChatWindowJSBridge(); virtual ~QtChatWindowJSBridge(); signals: - void buttonClicked(QString id, QString arg1, QString arg2, QString arg3); + void buttonClicked(QString id, QString arg1, QString arg2, QString arg3, QString arg4, QString arg5); }; } diff --git a/Swift/QtUI/QtInviteToChatWindow.cpp b/Swift/QtUI/QtInviteToChatWindow.cpp deleted file mode 100644 index ce6dea0..0000000 --- a/Swift/QtUI/QtInviteToChatWindow.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2012 Kevin Smith - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#include <Swift/QtUI/QtInviteToChatWindow.h> - -#include <QHBoxLayout> -#include <QCompleter> -#include <QLabel> -#include <QLineEdit> -#include <QPushButton> -#include <QDialogButtonBox> - -#include <Swift/QtUI/QtSwiftUtil.h> - -namespace Swift { - -QtInviteToChatWindow::QtInviteToChatWindow(QWidget* parent) : QDialog(parent) { - QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this); - //layout->setContentsMargins(0,0,0,0); - //layout->setSpacing(2); - - QLabel* description = new QLabel(tr("Users to invite to this chat (one per line):")); - layout->addWidget(description); - - jidsLayout_ = new QBoxLayout(QBoxLayout::TopToBottom); - layout->addLayout(jidsLayout_); - - QLabel* reasonLabel = new QLabel(tr("If you want to provide a reason for the invitation, enter it here")); - layout->addWidget(reasonLabel); - reason_ = new QLineEdit(this); - layout->addWidget(reason_); - - connect(this, SIGNAL(accepted()), this, SLOT(handleAccepting())); - connect(this, SIGNAL(rejected()), this, SLOT(handleRejecting())); - - - buttonBox_ = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - - connect(buttonBox_, SIGNAL(accepted()), this, SLOT(accept())); - connect(buttonBox_, SIGNAL(rejected()), this, SLOT(reject())); - - layout->addWidget(buttonBox_); - addJIDLine(); - - jids_[0]->setFocus(); - - setModal(false); - show(); -} - -QtInviteToChatWindow::~QtInviteToChatWindow() { - -} - -void QtInviteToChatWindow::handleAccepting() { - onCompleted(); -} - -void QtInviteToChatWindow::handleRejecting() { - onDismissed(); -} - -std::string QtInviteToChatWindow::getReason() const { - return Q2PSTRING(reason_->text()); -} - -std::vector<JID> QtInviteToChatWindow::getJIDs() const { - std::vector<JID> results; - foreach (QLineEdit* jidEdit, jids_) { - QStringList parts = jidEdit->text().split(" "); - if (parts.size() > 0) { - JID jid(Q2PSTRING(parts.last())); - if (jid.isValid() && !jid.getNode().empty()) { - results.push_back(jid); - } - } - } - return results; -} - -void QtInviteToChatWindow::addJIDLine() { - QLineEdit* jid = new QLineEdit(this); - QCompleter* completer = new QCompleter(&completions_, this); - completer->setCaseSensitivity(Qt::CaseInsensitive); - jid->setCompleter(completer); - jidsLayout_->addWidget(jid); - connect(jid, SIGNAL(textChanged(const QString&)), this, SLOT(handleJIDTextChanged())); - if (!jids_.empty()) { - setTabOrder(jids_.back(), jid); - } - jids_.push_back(jid); - setTabOrder(jid, reason_); - setTabOrder(reason_, buttonBox_); - //setTabOrder(buttonBox_, jids_[0]); -} - -void QtInviteToChatWindow::handleJIDTextChanged() { - bool gotEmpty = false; - foreach(QLineEdit* edit, jids_) { - if (edit->text().isEmpty()) { - gotEmpty = true; - } - } - if (!gotEmpty) { - addJIDLine(); - } -} - -typedef std::pair<JID, std::string> JIDString; - -void QtInviteToChatWindow::setAutoCompletions(std::vector<std::pair<JID, std::string> > completions) { - QStringList list; - foreach (JIDString jidPair, completions) { - QString line = P2QSTRING(jidPair.first.toString()); - if (jidPair.second != jidPair.first.toString() && !jidPair.second.empty()) { - line = P2QSTRING(jidPair.second) + " - " + line; - } - list.append(line); - } - completions_.setStringList(list); -} - -} - - - diff --git a/Swift/QtUI/QtInviteToChatWindow.h b/Swift/QtUI/QtInviteToChatWindow.h deleted file mode 100644 index dd8743a..0000000 --- a/Swift/QtUI/QtInviteToChatWindow.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2012 Kevin Smith - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. - */ - -#pragma once - -#include <Swift/Controllers/UIInterfaces/InviteToChatWindow.h> - -#include <QDialog> -#include <QStringListModel> - -class QLineEdit; -class QBoxLayout; -class QDialogButtonBox; - -namespace Swift { - class QtInviteToChatWindow : public QDialog, public InviteToChatWindow { - Q_OBJECT - public: - QtInviteToChatWindow(QWidget* parent = NULL); - virtual ~QtInviteToChatWindow(); - - virtual std::string getReason() const; - - virtual std::vector<JID> getJIDs() const; - virtual void setAutoCompletions(std::vector<std::pair<JID, std::string> > completions); - private: - void addJIDLine(); - private slots: - void handleJIDTextChanged(); - void handleAccepting(); - void handleRejecting(); - private: - QStringListModel completions_; - QLineEdit* reason_; - QBoxLayout* jidsLayout_; - std::vector<QLineEdit*> jids_; - QDialogButtonBox* buttonBox_; - }; -} - - diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp index 572b06f..6f87a88 100644 --- a/Swift/QtUI/QtMainWindow.cpp +++ b/Swift/QtUI/QtMainWindow.cpp @@ -144,6 +144,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr actionsMenu->addAction(openBlockingListEditor_); openBlockingListEditor_->setVisible(false); addUserAction_ = new QAction(tr("&Add Contact…"), this); + addUserAction_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_D)); connect(addUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleAddUserActionTriggered(bool))); actionsMenu->addAction(addUserAction_); editUserAction_ = new QAction(tr("&Edit Selected Contact…"), this); @@ -151,6 +152,7 @@ QtMainWindow::QtMainWindow(SettingsProvider* settings, UIEventStream* uiEventStr actionsMenu->addAction(editUserAction_); editUserAction_->setEnabled(false); chatUserAction_ = new QAction(tr("Start &Chat…"), this); + chatUserAction_->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_N)); connect(chatUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleChatUserActionTriggered(bool))); actionsMenu->addAction(chatUserAction_); serverAdHocMenu_ = new QMenu(tr("Run Server Command"), this); diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp index 0c7fbc2..e5db22d 100644 --- a/Swift/QtUI/QtUIFactory.cpp +++ b/Swift/QtUI/QtUIFactory.cpp @@ -145,7 +145,7 @@ void QtUIFactory::handleChatWindowFontResized(int size) { } UserSearchWindow* QtUIFactory::createUserSearchWindow(UserSearchWindow::Type type, UIEventStream* eventStream, const std::set<std::string>& groups) { - return new QtUserSearchWindow(eventStream, type, groups); + return new QtUserSearchWindow(eventStream, type, groups, qtOnlySettings); } JoinMUCWindow* QtUIFactory::createJoinMUCWindow(UIEventStream* uiEventStream) { diff --git a/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp b/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp index b586444..90520ad 100644 --- a/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp +++ b/Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.cpp @@ -15,12 +15,12 @@ QtRemovableItemDelegate::QtRemovableItemDelegate(const QStyle* style) : style(st } -void QtRemovableItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex&) const { +void QtRemovableItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOption opt; opt.state = QStyle::State(0); opt.state |= QStyle::State_MouseOver; painter->save(); - painter->fillRect(option.rect, option.state & QStyle::State_Selected ? option.palette.highlight() : option.palette.base()); + drawBackground(painter, option, index); painter->translate(option.rect.x(), option.rect.y()+(option.rect.height() - 12)/2); style->drawPrimitive(QStyle::PE_IndicatorTabClose, &opt, painter); painter->restore(); diff --git a/Swift/QtUI/Roster/QtTreeWidget.cpp b/Swift/QtUI/Roster/QtTreeWidget.cpp index 64d0fcf..99f1f34 100644 --- a/Swift/QtUI/Roster/QtTreeWidget.cpp +++ b/Swift/QtUI/Roster/QtTreeWidget.cpp @@ -42,6 +42,7 @@ QtTreeWidget::QtTreeWidget(UIEventStream* eventStream, SettingsProvider* setting #ifdef SWIFT_EXPERIMENTAL_FT setAcceptDrops(true); #endif + setDragEnabled(true); setRootIsDecorated(true); connect(this, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleItemActivated(const QModelIndex&))); connect(model_, SIGNAL(itemExpanded(const QModelIndex&, bool)), this, SLOT(handleModelItemExpanded(const QModelIndex&, bool))); @@ -157,7 +158,7 @@ void QtTreeWidget::dragMoveEvent(QDragMoveEvent* event) { } } } - event->ignore(); + QTreeView::dragMoveEvent(event); } void QtTreeWidget::handleExpanded(const QModelIndex& index) { diff --git a/Swift/QtUI/Roster/RosterModel.cpp b/Swift/QtUI/Roster/RosterModel.cpp index 1b93047..3791ffa 100644 --- a/Swift/QtUI/Roster/RosterModel.cpp +++ b/Swift/QtUI/Roster/RosterModel.cpp @@ -10,6 +10,7 @@ #include <QColor> #include <QIcon> +#include <QMimeData> #include <qdebug.h> #include "Swiften/Elements/StatusShow.h" @@ -55,7 +56,7 @@ void RosterModel::reLayout() { void RosterModel::handleChildrenChanged(GroupRosterItem* /*group*/) { reLayout(); -} +} void RosterModel::handleDataChanged(RosterItem* item) { Q_ASSERT(item); @@ -65,6 +66,14 @@ void RosterModel::handleDataChanged(RosterItem* item) { } } +Qt::ItemFlags RosterModel::flags(const QModelIndex& index) const { + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + if (dynamic_cast<GroupRosterItem*>(getItem(index)) == NULL) { + flags |= Qt::ItemIsDragEnabled; + } + return flags; +} + int RosterModel::columnCount(const QModelIndex& /*parent*/) const { return 1; } @@ -230,4 +239,25 @@ int RosterModel::rowCount(const QModelIndex& parent) const { return count; } +QMimeData* RosterModel::mimeData(const QModelIndexList& indexes) const { + QMimeData* data = QAbstractItemModel::mimeData(indexes); + + ContactRosterItem *item = dynamic_cast<ContactRosterItem*>(getItem(indexes.first())); + if (item == NULL) { + return data; + } + + QByteArray itemData; + QDataStream dataStream(&itemData, QIODevice::WriteOnly); + + // jid, chatName, activity, statusType, avatarPath + dataStream << P2QSTRING(item->getJID().toString()); + dataStream << P2QSTRING(item->getDisplayName()); + dataStream << P2QSTRING(item->getStatusText()); + dataStream << item->getSimplifiedStatusShow(); + dataStream << P2QSTRING(item->getAvatarPath().string()); + data->setData("application/vnd.swift.contact-jid", itemData); + return data; +} + } diff --git a/Swift/QtUI/Roster/RosterModel.h b/Swift/QtUI/Roster/RosterModel.h index 23d54f8..cae80c4 100644 --- a/Swift/QtUI/Roster/RosterModel.h +++ b/Swift/QtUI/Roster/RosterModel.h @@ -29,12 +29,15 @@ namespace Swift { RosterModel(QtTreeWidget* view); ~RosterModel(); void setRoster(Roster* swiftRoster); + Qt::ItemFlags flags(const QModelIndex& index) const; 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 index(RosterItem* item) const; QModelIndex parent(const QModelIndex& index) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; + QMimeData* mimeData(const QModelIndexList& indexes) const; + signals: void itemExpanded(const QModelIndex& item, bool expanded); private: diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 1c3fd70..86efb51 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -136,7 +136,6 @@ sources = [ "QtFormResultItemModel.cpp", "QtLineEdit.cpp", "QtJoinMUCWindow.cpp", - "QtInviteToChatWindow.cpp", "QtConnectionSettingsWindow.cpp", "Roster/RosterModel.cpp", "Roster/QtTreeWidget.cpp", @@ -162,7 +161,12 @@ sources = [ "MUCSearch/MUCSearchRoomItem.cpp", "MUCSearch/MUCSearchEmptyItem.cpp", "MUCSearch/MUCSearchDelegate.cpp", + "UserSearch/ContactListDelegate.cpp", + "UserSearch/ContactListModel.cpp", + "UserSearch/QtContactListWidget.cpp", + "UserSearch/QtSuggestingJIDInput.cpp", "UserSearch/QtUserSearchFirstPage.cpp", + "UserSearch/QtUserSearchFirstMultiJIDPage.cpp", "UserSearch/QtUserSearchFieldsPage.cpp", "UserSearch/QtUserSearchResultsPage.cpp", "UserSearch/QtUserSearchDetailsPage.cpp", @@ -267,6 +271,7 @@ else : myenv.Uic4("MUCSearch/QtMUCSearchWindow.ui") myenv.Uic4("UserSearch/QtUserSearchWizard.ui") myenv.Uic4("UserSearch/QtUserSearchFirstPage.ui") +myenv.Uic4("UserSearch/QtUserSearchFirstMultiJIDPage.ui") myenv.Uic4("UserSearch/QtUserSearchFieldsPage.ui") myenv.Uic4("UserSearch/QtUserSearchResultsPage.ui") myenv.Uic4("QtBookmarkDetailWindow.ui") diff --git a/Swift/QtUI/UserSearch/ContactListDelegate.cpp b/Swift/QtUI/UserSearch/ContactListDelegate.cpp new file mode 100644 index 0000000..29cab83 --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListDelegate.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/QtUI/UserSearch/ContactListModel.h> +#include <Swift/Controllers/Contact.h> +#include <Swift/QtUI/QtSwiftUtil.h> + +namespace Swift { + +ContactListDelegate::ContactListDelegate(bool compact) : compact_(compact) { +} + +ContactListDelegate::~ContactListDelegate() { +} + +void ContactListDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if (!index.isValid()) { + return; + } + const Contact* contact = static_cast<Contact*>(index.internalPointer()); + QColor nameColor = index.data(Qt::TextColorRole).value<QColor>(); + QString avatarPath = index.data(ContactListModel::AvatarRole).value<QString>(); + QIcon presenceIcon =index.data(ChatListRecentItem::PresenceIconRole).isValid() && !index.data(ChatListRecentItem::PresenceIconRole).value<QIcon>().isNull() + ? index.data(ChatListRecentItem::PresenceIconRole).value<QIcon>() + : QIcon(":/icons/offline.png"); + QString name = P2QSTRING(contact->name); + QString statusText = P2QSTRING(contact->jid.toString()); + common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, false, 0, compact_); +} + +QSize ContactListDelegate::sizeHint(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 ContactListDelegate::setCompact(bool compact) { + compact_ = compact; +} + +} diff --git a/Swift/QtUI/UserSearch/ContactListDelegate.h b/Swift/QtUI/UserSearch/ContactListDelegate.h new file mode 100644 index 0000000..7680aba --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListDelegate.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QStyledItemDelegate> + +#include <Swift/QtUI/Roster/DelegateCommons.h> + +namespace Swift { + +class ContactListDelegate : public QStyledItemDelegate { + public: + ContactListDelegate(bool compact); + virtual ~ContactListDelegate(); + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + public slots: + void setCompact(bool compact); + + private: + bool compact_; + DelegateCommons common_; +}; +} diff --git a/Swift/QtUI/UserSearch/ContactListModel.cpp b/Swift/QtUI/UserSearch/ContactListModel.cpp new file mode 100644 index 0000000..6523a4d --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListModel.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/ContactListModel.h> + +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swiften/Base/Path.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/Elements/StatusShow.h> + +#include <QMimeData> + +namespace Swift { + +QDataStream& operator >>(QDataStream& in, StatusShow::Type& e){ + quint32 buffer; + in >> buffer; + switch(buffer) { + case StatusShow::Online: + e = StatusShow::Online; + break; + case StatusShow::Away: + e = StatusShow::Away; + break; + case StatusShow::FFC: + e = StatusShow::FFC; + break; + case StatusShow::XA: + e = StatusShow::XA; + break; + case StatusShow::DND: + e = StatusShow::DND; + break; + default: + e = StatusShow::None; + break; + } + return in; +} + +ContactListModel::ContactListModel(bool editable) : QAbstractItemModel(), editable_(editable) { +} + +void ContactListModel::setList(const std::vector<Contact>& list) { + emit layoutAboutToBeChanged(); + contacts_ = list; + emit layoutChanged(); +} + +const std::vector<Contact>& ContactListModel::getList() const { + return contacts_; +} + +Qt::ItemFlags ContactListModel::flags(const QModelIndex& index) const { + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + if (index.isValid()) { + flags = flags & ~Qt::ItemIsDropEnabled; + } else { + flags = Qt::ItemIsDropEnabled | flags; + } + return flags; +} + +int ContactListModel::columnCount(const QModelIndex&) const { + return editable_ ? 2 : 1; +} + +QVariant ContactListModel::data(const QModelIndex& index, int role) const { + if (boost::numeric_cast<size_t>(index.row()) < contacts_.size()) { + const Contact& contact = contacts_[index.row()]; + if (role == Qt::EditRole) { + return P2QSTRING(contact.jid.toString()); + } + return dataForContact(contact, role); + } else { + return QVariant(); + } +} + +bool ContactListModel::dropMimeData(const QMimeData* data, Qt::DropAction /*action*/, int /*row*/, int /*column*/, const QModelIndex& /*parent*/) { + if (!data->hasFormat("application/vnd.swift.contact-jid")) { + return false; + } + + QByteArray dataBytes = data->data("application/vnd.swift.contact-jid"); + QDataStream dataStream(&dataBytes, QIODevice::ReadOnly); + QString jidString; + QString displayName; + QString statusText; + StatusShow::Type statusType; + QString avatarPath; + + dataStream >> jidString; + dataStream >> displayName; + dataStream >> statusText; + dataStream >> statusType; + dataStream >> avatarPath; + + JID jid = JID(Q2PSTRING(jidString)); + + foreach(const Contact& contact, contacts_) { + if (contact.jid == jid) { + return false; + } + } + + emit layoutAboutToBeChanged(); + contacts_.push_back(Contact(Q2PSTRING(displayName), jid, statusType, Q2PSTRING(avatarPath))); + emit layoutChanged(); + + onJIDsDropped(std::vector<JID>(1, jid)); + onListChanged(getList()); + + return true; +} + +QModelIndex ContactListModel::index(int row, int column, const QModelIndex& parent) const { + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + return boost::numeric_cast<size_t>(row) < contacts_.size() ? createIndex(row, column, (void*)&(contacts_[row])) : QModelIndex(); +} + +QModelIndex ContactListModel::parent(const QModelIndex& index) const { + if (!index.isValid()) { + return QModelIndex(); + } + return QModelIndex(); +} + +int ContactListModel::rowCount(const QModelIndex& /*parent*/) const { + return contacts_.size(); +} + +bool ContactListModel::removeRows(int row, int /*count*/, const QModelIndex& /*parent*/) { + if (boost::numeric_cast<size_t>(row) < contacts_.size()) { + emit layoutAboutToBeChanged(); + contacts_.erase(contacts_.begin() + row); + emit layoutChanged(); + onListChanged(getList()); + return true; + } + return false; +} + +QVariant ContactListModel::dataForContact(const Contact& contact, int role) const { + switch (role) { + case Qt::DisplayRole: return P2QSTRING(contact.name); + case DetailTextRole: return P2QSTRING(contact.jid.toString()); + case AvatarRole: return QVariant(P2QSTRING(pathToString(contact.avatarPath))); + case PresenceIconRole: return getPresenceIconForContact(contact); + default: return QVariant(); + } +} + +QIcon ContactListModel::getPresenceIconForContact(const Contact& contact) const { + QString iconString; + switch (contact.statusType) { + case StatusShow::Online: iconString = "online";break; + case StatusShow::Away: iconString = "away";break; + case StatusShow::XA: iconString = "away";break; + case StatusShow::FFC: iconString = "online";break; + case StatusShow::DND: iconString = "dnd";break; + case StatusShow::None: iconString = "offline";break; + } + return QIcon(":/icons/" + iconString + ".png"); +} + +} diff --git a/Swift/QtUI/UserSearch/ContactListModel.h b/Swift/QtUI/UserSearch/ContactListModel.h new file mode 100644 index 0000000..e7f4a0b --- /dev/null +++ b/Swift/QtUI/UserSearch/ContactListModel.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <vector> +#include <boost/bind.hpp> +#include <Swiften/Base/boost_bsignals.h> + +#include <QAbstractItemModel> + +#include <Swift/Controllers/Contact.h> +#include <Swift/QtUI/ChatList/ChatListItem.h> +#include <Swift/QtUI/ChatList/ChatListGroupItem.h> +#include <Swift/QtUI/ChatList/ChatListRecentItem.h> + +namespace Swift { + class ContactListModel : public QAbstractItemModel { + Q_OBJECT + public: + enum ContactRoles { + DetailTextRole = Qt::UserRole, + AvatarRole = Qt::UserRole + 1, + PresenceIconRole = Qt::UserRole + 2 + }; + + public: + ContactListModel(bool editable); + + void setList(const std::vector<Contact>& list); + const std::vector<Contact>& getList() const; + + Qt::ItemFlags flags(const QModelIndex& index) const; + int columnCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + int rowCount(const QModelIndex& parent = QModelIndex()) const; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()); + + private: + QVariant dataForContact(const Contact& contact, int role) const; + QIcon getPresenceIconForContact(const Contact& contact) const; + + signals: + void onListChanged(std::vector<Contact> list); + void onJIDsDropped(const std::vector<JID>& contact); + + private: + bool editable_; + std::vector<Contact> contacts_; + }; + +} diff --git a/Swift/QtUI/UserSearch/QtContactListWidget.cpp b/Swift/QtUI/UserSearch/QtContactListWidget.cpp new file mode 100644 index 0000000..899c592 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtContactListWidget.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> + +#include <Swift/QtUI/UserSearch/ContactListModel.h> +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/QtUI/QtUISettingConstants.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/QtVCardWidget/QtRemovableItemDelegate.h> + +#include <QHeaderView> + +namespace Swift { + +QtContactListWidget::QtContactListWidget(QWidget* parent, SettingsProvider* settings) : QTreeView(parent), settings_(settings), limited_(false) { + contactListModel_ = new ContactListModel(true); + setModel(contactListModel_); + + connect(contactListModel_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(handleListChanged(std::vector<Contact>))); + connect(contactListModel_, SIGNAL(onListChanged(std::vector<Contact>)), this, SIGNAL(onListChanged(std::vector<Contact>))); + connect(contactListModel_, SIGNAL(onJIDsDropped(std::vector<JID>)), this, SIGNAL(onJIDsAdded(std::vector<JID>))); + + setSelectionMode(QAbstractItemView::SingleSelection); + setSelectionBehavior(QAbstractItemView::SelectRows); + setDragEnabled(true); + setAcceptDrops(true); + setDropIndicatorShown(true); + setUniformRowHeights(true); + + setAlternatingRowColors(true); + setIndentation(0); + setHeaderHidden(true); + setExpandsOnDoubleClick(false); + setItemsExpandable(false); + setEditTriggers(QAbstractItemView::DoubleClicked); + + contactListDelegate_ = new ContactListDelegate(settings->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + removableItemDelegate_ = new QtRemovableItemDelegate(style()); + + setItemDelegateForColumn(0, contactListDelegate_); + setItemDelegateForColumn(1, removableItemDelegate_); + + int closeIconWidth = fontMetrics().height(); + header()->resizeSection(1, closeIconWidth); + + header()->setStretchLastSection(false); +#if QT_VERSION >= 0x050000 + header()->setSectionResizeMode(0, QHeaderView::Stretch); +#else + header()->setResizeMode(0, QHeaderView::Stretch); +#endif +} + +QtContactListWidget::~QtContactListWidget() { + delete contactListDelegate_; + delete removableItemDelegate_; +} + +void QtContactListWidget::setList(const std::vector<Contact>& list) { + contactListModel_->setList(list); +} + +std::vector<Contact> QtContactListWidget::getList() const { + return contactListModel_->getList(); +} + +void QtContactListWidget::setMaximumNoOfContactsToOne(bool limited) { + limited_ = limited; + if (limited) { + handleListChanged(getList()); + } else { + setAcceptDrops(true); + setDropIndicatorShown(true); + } +} + +void QtContactListWidget::updateContacts(const std::vector<Contact>& contactUpdates) { + std::vector<Contact> contacts = contactListModel_->getList(); + foreach(const Contact& contactUpdate, contactUpdates) { + for(size_t n = 0; n < contacts.size(); n++) { + if (contactUpdate.jid == contacts[n].jid) { + contacts[n] = contactUpdate; + break; + } + } + } + contactListModel_->setList(contacts); +} + +void QtContactListWidget::handleListChanged(std::vector<Contact> list) { + if (limited_) { + setAcceptDrops(list.size() <= 1); + setDropIndicatorShown(list.size() <= 1); + } +} + +void QtContactListWidget::handleSettingsChanged(const std::string&) { + contactListDelegate_->setCompact(settings_->getSetting(QtUISettingConstants::COMPACT_ROSTER)); +} + +} diff --git a/Swift/QtUI/UserSearch/QtContactListWidget.h b/Swift/QtUI/UserSearch/QtContactListWidget.h new file mode 100644 index 0000000..f360a91 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtContactListWidget.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <vector> + +#include <QTreeView> + +#include <Swift/Controllers/Contact.h> +#include <Swiften/Base/Log.h> + +#include <QDragEnterEvent> +#include <QDragMoveEvent> +#include <QDropEvent> + +namespace Swift { + +class ContactListDelegate; +class ContactListModel; +class SettingsProvider; +class QtRemovableItemDelegate; + +class QtContactListWidget : public QTreeView { + Q_OBJECT +public: + QtContactListWidget(QWidget* parent, SettingsProvider* settings); + virtual ~QtContactListWidget(); + + void setList(const std::vector<Contact>& list); + std::vector<Contact> getList() const; + void setMaximumNoOfContactsToOne(bool limited); + +public slots: + void updateContacts(const std::vector<Contact>& contactUpdates); + +signals: + void onListChanged(std::vector<Contact> list); + void onJIDsAdded(const std::vector<JID>& jids); + +private slots: + void handleListChanged(std::vector<Contact> list); + +private: + void handleSettingsChanged(const std::string&); + +private: + SettingsProvider* settings_; + ContactListModel* contactListModel_; + ContactListDelegate* contactListDelegate_; + QtRemovableItemDelegate* removableItemDelegate_; + bool limited_; +}; + +} diff --git a/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp new file mode 100644 index 0000000..ca65dca --- /dev/null +++ b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> +#include <Swift/QtUI/UserSearch/ContactListDelegate.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/QtUISettingConstants.h> +#include <Swift/QtUI/UserSearch/ContactListModel.h> + +#include <Swiften/Base/boost_bsignals.h> +#include <boost/bind.hpp> + +#include <Swift/QtUI/QtSwiftUtil.h> + +#include <QAbstractItemView> +#include <QApplication> +#include <QDesktopWidget> +#include <QKeyEvent> + + +namespace Swift { + +QtSuggestingJIDInput::QtSuggestingJIDInput(QWidget* parent, SettingsProvider* settings) : QLineEdit(parent), settings_(settings), currentContact_(NULL) { + treeViewPopup_ = new QTreeView(); + treeViewPopup_->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); + //treeViewPopup_->setAttribute(Qt::WA_ShowWithoutActivating); + treeViewPopup_->setAlternatingRowColors(true); + treeViewPopup_->setIndentation(0); + treeViewPopup_->setHeaderHidden(true); + treeViewPopup_->setExpandsOnDoubleClick(false); + treeViewPopup_->setItemsExpandable(false); + treeViewPopup_->setSelectionMode(QAbstractItemView::SingleSelection); + treeViewPopup_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + treeViewPopup_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + QSizePolicy policy(treeViewPopup_->sizePolicy()); + policy.setVerticalPolicy(QSizePolicy::Expanding); + treeViewPopup_->setSizePolicy(policy); + treeViewPopup_->hide(); + treeViewPopup_->setFocusProxy(this); + connect(treeViewPopup_, SIGNAL(clicked(QModelIndex)), this, SLOT(handleClicked(QModelIndex))); + connect(treeViewPopup_, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(handleClicked(QModelIndex))); + + contactListModel_ = new ContactListModel(false); + treeViewPopup_->setModel(contactListModel_); + + contactListDelegate_ = new ContactListDelegate(settings->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + treeViewPopup_->setItemDelegate(contactListDelegate_); + + settings_->onSettingChanged.connect(boost::bind(&QtSuggestingJIDInput::handleSettingsChanged, this, _1)); +} + +QtSuggestingJIDInput::~QtSuggestingJIDInput() { + settings_->onSettingChanged.disconnect(boost::bind(&QtSuggestingJIDInput::handleSettingsChanged, this, _1)); + delete treeViewPopup_; +} + +const Contact* QtSuggestingJIDInput::getContact() { + if (currentContact_ != NULL) { + return currentContact_; + } else { + if (!text().isEmpty()) { + JID jid(Q2PSTRING(text())); + if (jid.isValid()) { + manualContact_.name = jid.toString(); + manualContact_.jid = jid; + return &manualContact_; + } + } + return NULL; + } +} + +void QtSuggestingJIDInput::setSuggestions(const std::vector<Contact>& suggestions) { + contactListModel_->setList(suggestions); + positionPopup(); + if (!suggestions.empty()) { + treeViewPopup_->setCurrentIndex(contactListModel_->index(0, 0)); + showPopup(); + } else { + currentContact_ = NULL; + } +} + +void QtSuggestingJIDInput::keyPressEvent(QKeyEvent* event) { + if (event->key() == Qt::Key_Up) { + if (contactListModel_->rowCount() > 0) { + int row = treeViewPopup_->currentIndex().row(); + row = (row + contactListModel_->rowCount() - 1) % contactListModel_->rowCount(); + treeViewPopup_->setCurrentIndex(contactListModel_->index(row, 0)); + } + } else if (event->key() == Qt::Key_Down) { + if (contactListModel_->rowCount() > 0) { + int row = treeViewPopup_->currentIndex().row(); + row = (row + contactListModel_->rowCount() + 1) % contactListModel_->rowCount(); + treeViewPopup_->setCurrentIndex(contactListModel_->index(row, 0)); + } + } else if (event->key() == Qt::Key_Return && treeViewPopup_->isVisible()) { + QModelIndex index = treeViewPopup_->currentIndex(); + if (!contactListModel_->getList().empty() && index.isValid()) { + currentContact_ = &contactListModel_->getList()[index.row()]; + setText(P2QSTRING(currentContact_->jid.toString())); + hidePopup(); + clearFocus(); + } else { + currentContact_ = NULL; + } + editingDone(); + } else { + QLineEdit::keyPressEvent(event); + } +} + +void QtSuggestingJIDInput::handleApplicationFocusChanged(QWidget* /*old*/, QWidget* /*now*/) { + /* Using the now argument gives use the wrong widget. This is part of the code needed + to prevent stealing of focus when opening a the suggestion window. */ + QWidget* now = qApp->focusWidget(); + if (!now || (now != treeViewPopup_ && now != this && !now->isAncestorOf(this) && !now->isAncestorOf(treeViewPopup_) && !this->isAncestorOf(now) && !treeViewPopup_->isAncestorOf(now))) { + hidePopup(); + } +} + +void QtSuggestingJIDInput::handleSettingsChanged(const std::string& setting) { + if (setting == QtUISettingConstants::COMPACT_ROSTER.getKey()) { + contactListDelegate_->setCompact(settings_->getSetting(QtUISettingConstants::COMPACT_ROSTER)); + } +} + +void QtSuggestingJIDInput::handleClicked(const QModelIndex& index) { + if (index.isValid()) { + currentContact_ = &contactListModel_->getList()[index.row()]; + setText(P2QSTRING(currentContact_->jid.toString())); + hidePopup(); + } +} + +void QtSuggestingJIDInput::positionPopup() { + QDesktopWidget* desktop = QApplication::desktop(); + int screen = desktop->screenNumber(this); + QPoint point = mapToGlobal(QPoint(0, height())); + QRect geometry = desktop->availableGeometry(screen); + int x = point.x(); + int y = point.y(); + int width = this->width(); + int height = 80; + + int screenWidth = geometry.x() + geometry.width(); + if (x + width > screenWidth) { + x = screenWidth - width; + } + + height = treeViewPopup_->sizeHintForRow(0) * contactListModel_->rowCount(); + height = height > 200 ? 200 : height; + + int marginLeft; + int marginTop; + int marginRight; + int marginBottom; + treeViewPopup_->getContentsMargins(&marginLeft, &marginTop, &marginRight, &marginBottom); + height += marginTop + marginBottom; + width += marginLeft + marginRight; + + treeViewPopup_->setGeometry(x, y, width, height); + treeViewPopup_->move(x, y); + treeViewPopup_->setMaximumWidth(width); +} + +void QtSuggestingJIDInput::showPopup() { + treeViewPopup_->show(); + activateWindow(); + connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(handleApplicationFocusChanged(QWidget*, QWidget*)), Qt::QueuedConnection); + setFocus(); +} + +void QtSuggestingJIDInput::hidePopup() { + disconnect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)), this, SLOT(handleApplicationFocusChanged(QWidget*, QWidget*))); + treeViewPopup_->hide(); +} + +} diff --git a/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h new file mode 100644 index 0000000..673621c --- /dev/null +++ b/Swift/QtUI/UserSearch/QtSuggestingJIDInput.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QLineEdit> +#include <QTreeView> + +#include <Swift/Controllers/Contact.h> + +namespace Swift { + +class ContactListDelegate; +class SettingsProvider; +class ContactListModel; + +class QtSuggestingJIDInput : public QLineEdit { + Q_OBJECT + public: + QtSuggestingJIDInput(QWidget* parent, SettingsProvider* settings); + virtual ~QtSuggestingJIDInput(); + + const Contact* getContact(); + + void setSuggestions(const std::vector<Contact>& suggestions); + + signals: + void editingDone(); + + protected: + virtual void keyPressEvent(QKeyEvent* event); + + private: + void handleSettingsChanged(const std::string& setting); + + private slots: + void handleClicked(const QModelIndex& index); + void handleApplicationFocusChanged(QWidget* old, QWidget* now); + + private: + void positionPopup(); + void showPopup(); + void hidePopup(); + + private: + SettingsProvider* settings_; + ContactListModel* contactListModel_; + QTreeView* treeViewPopup_; + ContactListDelegate* contactListDelegate_; + Contact manualContact_; + const Contact* currentContact_; +}; + +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp new file mode 100644 index 0000000..b1e9a12 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#include "Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h" + +#include "Swift/QtUI/QtSwiftUtil.h" +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> + +namespace Swift { + +QtUserSearchFirstMultiJIDPage::QtUserSearchFirstMultiJIDPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings) { + setupUi(this); + setTitle(title); + QString introText = ""; + switch (type) { + case UserSearchWindow::AddContact: + introText = tr("Add another user to your contact list"); + break; + case UserSearchWindow::ChatToContact: + introText = tr("Chat to another user"); + break; + case UserSearchWindow::InviteToChat: + introText = tr("Invite contact to chat"); + break; + } + + setSubTitle(QString(tr("%1. If you know their address you can enter it directly, or you can search for them.")).arg(introText)); + + contactList_ = new QtContactListWidget(this, settings); + horizontalLayout_5->addWidget(contactList_); + + jid_ = new QtSuggestingJIDInput(this, settings); + horizontalLayout_6->insertWidget(0, jid_); + + connect(contactList_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(emitCompletenessCheck())); + connect(jid_, SIGNAL(editingDone()), this, SLOT(handleEditingDone())); +} + +bool QtUserSearchFirstMultiJIDPage::isComplete() const { + return !contactList_->getList().empty(); +} + +void QtUserSearchFirstMultiJIDPage::emitCompletenessCheck() { + emit completeChanged(); +} + +void QtUserSearchFirstMultiJIDPage::handleEditingDone() { + addContactButton_->setFocus(); +} + +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h new file mode 100644 index 0000000..427995e --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +#pragma once + +#include <QWizardPage> + +#include <Swift/QtUI/UserSearch/ui_QtUserSearchFirstMultiJIDPage.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> + +namespace Swift { + class UserSearchModel; + class UserSearchDelegate; + class UserSearchResult; + class UIEventStream; + class QtContactListWidget; + class ContactSuggester; + class AvatarManager; + class VCardManager; + class SettingsProvider; + class QtSuggestingJIDInput; + + class QtUserSearchFirstMultiJIDPage : public QWizardPage, public Ui::QtUserSearchFirstMultiJIDPage { + Q_OBJECT + public: + QtUserSearchFirstMultiJIDPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings); + virtual bool isComplete() const; + + public slots: + void emitCompletenessCheck(); + void handleEditingDone(); + + public: + QtContactListWidget* contactList_; + QtSuggestingJIDInput* jid_; + }; +} diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui new file mode 100644 index 0000000..4a87f41 --- /dev/null +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.ui @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QtUserSearchFirstMultiJIDPage</class> + <widget class="QWizardPage" name="QtUserSearchFirstMultiJIDPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>477</width> + <height>500</height> + </rect> + </property> + <property name="windowTitle"> + <string/> + </property> + <property name="title"> + <string>Add a user</string> + </property> + <property name="subTitle"> + <string>Add another user to your contact list. If you know their address you can add them directly, or you can search for them.</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="howLabel_"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"/> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="title"> + <string>Choose another contact</string> + </property> + <property name="flat"> + <bool>false</bool> + </property> + <property name="checkable"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>-1</number> + </property> + <property name="margin"> + <number>6</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0"> + <property name="spacing"> + <number>-1</number> + </property> + <item> + <widget class="QPushButton" name="addContactButton_"> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string>Add Contact</string> + </property> + <property name="default"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QRadioButton" name="byLocalSearch_"> + <property name="text"> + <string>I'd like to search my server</string> + </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> + </widget> + </item> + <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> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QRadioButton" name="byRemoteSearch_"> + <property name="text"> + <string>I'd like to search another server:</string> + </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="service_"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <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="addViaSearchButton_"> + <property name="text"> + <string>Add via Search</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Reason:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="reason_"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>77</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="errorLabel_"> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </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> + <resources/> + <connections/> +</ui> diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp index 7a91a98..af53a26 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.cpp @@ -8,12 +8,19 @@ #include "Swift/QtUI/QtSwiftUtil.h" +#include <Swiften/Base/Log.h> + namespace Swift { -QtUserSearchFirstPage::QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title) { +QtUserSearchFirstPage::QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings) { setupUi(this); setTitle(title); setSubTitle(QString(tr("%1. If you know their address you can enter it directly, or you can search for them.")).arg(type == UserSearchWindow::AddContact ? tr("Add another user to your contact list") : tr("Chat to another user"))); + jid_ = new QtSuggestingJIDInput(this, settings); + horizontalLayout_2->addWidget(jid_); + setTabOrder(byJID_, jid_); + setTabOrder(jid_, byLocalSearch_); + setTabOrder(byLocalSearch_, byRemoteSearch_); connect(jid_, SIGNAL(textChanged(const QString&)), this, SLOT(emitCompletenessCheck())); connect(service_->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(emitCompletenessCheck())); } diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h index d23b87d..d7487b0 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.h @@ -11,6 +11,8 @@ #include <Swift/QtUI/UserSearch/ui_QtUserSearchFirstPage.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> +#include <Swift/QtUI/UserSearch/QtSuggestingJIDInput.h> + namespace Swift { class UserSearchModel; class UserSearchDelegate; @@ -20,9 +22,11 @@ namespace Swift { class QtUserSearchFirstPage : public QWizardPage, public Ui::QtUserSearchFirstPage { Q_OBJECT public: - QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title); + QtUserSearchFirstPage(UserSearchWindow::Type type, const QString& title, SettingsProvider* settings); virtual bool isComplete() const; public slots: void emitCompletenessCheck(); + public: + QtSuggestingJIDInput* jid_; }; } diff --git a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui index bb0a625..24d401e 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui +++ b/Swift/QtUI/UserSearch/QtUserSearchFirstPage.ui @@ -34,11 +34,11 @@ <property name="text"> <string>I know their address:</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> - <item> - <widget class="QLineEdit" name="jid_"/> - </item> </layout> </item> <item> @@ -48,6 +48,9 @@ <property name="text"> <string>I'd like to search my server</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> <item> @@ -72,6 +75,9 @@ <property name="text"> <string>I'd like to search another server:</string> </property> + <property name="autoExclusive"> + <bool>true</bool> + </property> </widget> </item> <item> diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp index 73514fd..d06fa19 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp @@ -13,57 +13,50 @@ #include <boost/smart_ptr/make_shared.hpp> #include <Swiften/Base/foreach.h> -#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" -#include "Swift/QtUI/QtFormResultItemModel.h" -#include "QtUserSearchFirstPage.h" -#include "QtUserSearchFieldsPage.h" -#include "QtUserSearchResultsPage.h" -#include "QtUserSearchDetailsPage.h" +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/AddContactUIEvent.h> +#include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> +#include <Swift/QtUI/UserSearch/UserSearchModel.h> +#include <Swift/QtUI/UserSearch/UserSearchDelegate.h> +#include <Swift/QtUI/QtSwiftUtil.h> +#include <Swift/QtUI/QtFormResultItemModel.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFirstPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFirstMultiJIDPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchFieldsPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchResultsPage.h> +#include <Swift/QtUI/UserSearch/QtUserSearchDetailsPage.h> +#include <Swift/QtUI/UserSearch/QtContactListWidget.h> + +#include <Swiften/Base/Log.h> namespace Swift { -QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups) : eventStream_(eventStream), type_(type), model_(NULL) { +QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups, SettingsProvider* settingsProvider) : eventStream_(eventStream), type_(type), model_(NULL), settings_(settingsProvider), searchNext_(false), supportsImpromptu_(false) { setupUi(this); #ifndef Q_OS_MAC setWindowIcon(QIcon(":/logo-icon-16.png")); #endif - QString title(type == UserSearchWindow::AddContact ? tr("Add Contact") : tr("Chat to User")); + QString title; + switch(type) { + case AddContact: + title = tr("Add Contact"); + break; + case ChatToContact: + title = tr("Chat to Users"); + break; + case InviteToChat: + title = tr("Add Users to Chat"); + break; + } setWindowTitle(title); delegate_ = new UserSearchDelegate(); - firstPage_ = new QtUserSearchFirstPage(type, title); - connect(firstPage_->byJID_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); - connect(firstPage_->byLocalSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); - connect(firstPage_->byRemoteSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); -#if QT_VERSION >= 0x040700 - firstPage_->jid_->setPlaceholderText(tr("alice@wonderland.lit")); -#endif - firstPage_->service_->setEnabled(false); - setPage(1, firstPage_); - - fieldsPage_ = new QtUserSearchFieldsPage(); - fieldsPage_->fetchingThrobber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), this)); - fieldsPage_->fetchingThrobber_->movie()->stop(); - setPage(2, fieldsPage_); - - resultsPage_ = new QtUserSearchResultsPage(); - -#ifdef SWIFT_PLATFORM_MACOSX - resultsPage_->results_->setAlternatingRowColors(true); -#endif - if (type == AddContact) { - connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); - } - else { - connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(accept())); - } - setPage(3, resultsPage_); + setFirstPage(title); + setSecondPage(); + setThirdPage(); detailsPage_ = new QtUserSearchDetailsPage(groups); setPage(4, detailsPage_); @@ -78,16 +71,34 @@ QtUserSearchWindow::~QtUserSearchWindow() { } void QtUserSearchWindow::handleCurrentChanged(int page) { + searchNext_ = false; resultsPage_->emitCompletenessCheck(); - if (page == 2 && lastPage_ == 1) { + if (page == 1 && lastPage_ == 3) { + addSearchedJIDToList(getContactJID()); + setSecondPage(); + } + else if (page == 2 && lastPage_ == 1) { setError(""); /* next won't be called if JID is selected */ JID server = getServerToSearch(); clearForm(); onFormRequested(server); + setThirdPage(); } else if (page == 3 && lastPage_ == 2) { + JID server = getServerToSearch(); handleSearch(); + + if (type_ == AddContact) { + bool remote = firstPage_->byRemoteSearch_->isChecked(); + firstPage_->byRemoteSearch_->setChecked(remote); + firstPage_->service_->setEditText(P2QSTRING(server.toString())); + } else { + bool remote = firstMultiJIDPage_->byRemoteSearch_->isChecked(); + setFirstPage(); + firstMultiJIDPage_->byRemoteSearch_->setChecked(remote); + firstMultiJIDPage_->service_->setEditText(P2QSTRING(server.toString())); + } } else if (page == 4) { detailsPage_->clear(); @@ -98,28 +109,77 @@ void QtUserSearchWindow::handleCurrentChanged(int page) { } JID QtUserSearchWindow::getServerToSearch() { - return firstPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstPage_->service_->currentText().trimmed())) : myServer_; + if (type_ == AddContact) { + return firstPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstPage_->service_->currentText().trimmed())) : myServer_; + } else { + return firstMultiJIDPage_->byRemoteSearch_->isChecked() ? JID(Q2PSTRING(firstMultiJIDPage_->service_->currentText().trimmed())) : myServer_; + } } void QtUserSearchWindow::handleAccepted() { - JID jid = getContactJID(); + JID jid; + std::vector<JID> jids; + switch(type_) { + case AddContact: + jid = getContactJID(); + eventStream_->send(boost::make_shared<AddContactUIEvent>(jid, detailsPage_->getName(), detailsPage_->getSelectedGroups())); + break; + case ChatToContact: + if (contactVector_.size() == 1) { + boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(contactVector_[0].jid)); + eventStream_->send(event); + break; + } - if (type_ == AddContact) { - eventStream_->send(boost::make_shared<AddContactUIEvent>(jid, detailsPage_->getName(), detailsPage_->getSelectedGroups())); + foreach(const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + + eventStream_->send(boost::make_shared<CreateImpromptuMUCUIEvent>(jids, JID(), Q2PSTRING(firstMultiJIDPage_->reason_->text()))); + break; + case InviteToChat: + foreach(const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + eventStream_->send(boost::make_shared<InviteToMUCUIEvent>(roomJID_, jids, Q2PSTRING(firstMultiJIDPage_->reason_->text()))); + break; } - else { - boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(jid)); - eventStream_->send(event); +} + +void QtUserSearchWindow::handleContactSuggestionRequested(const QString& text) { + std::string stdText = Q2PSTRING(text); + onContactSuggestionsRequested(stdText); +} + +void QtUserSearchWindow::addContact() { + if (firstMultiJIDPage_->jid_->getContact() != 0) { + Contact contact = *(firstMultiJIDPage_->jid_->getContact()); + contactVector_.push_back(contact); + } + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->emitCompletenessCheck(); + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); } } int QtUserSearchWindow::nextId() const { - switch (currentId()) { - case 1: return firstPage_->byJID_->isChecked() ? (type_ == AddContact ? 4 : -1) : 2; - case 2: return 3; - case 3: return type_ == AddContact ? 4 : -1; - case 4: return -1; - default: return -1; + if (type_ == AddContact) { + switch (currentId()) { + case 1: return firstPage_->byJID_->isChecked() ? (type_ == AddContact ? 4 : -1) : 2; + case 2: return 3; + case 3: return type_ == AddContact ? 4 : -1; + case 4: return -1; + default: return -1; + } + } else { + switch (currentId()) { + case 1: return searchNext_ ? 2 : -1; + case 2: return 3; + case 3: return 1; + case 4: return -1; + default: return -1; + } } } @@ -167,7 +227,15 @@ void QtUserSearchWindow::handleSearch() { JID QtUserSearchWindow::getContactJID() const { JID jid; - if (!firstPage_->byJID_->isChecked()) { + + bool useSearchResult; + if (type_ == AddContact) { + useSearchResult = !firstPage_->byJID_->isChecked(); + } else { + useSearchResult = true; + } + + if (useSearchResult) { if (dynamic_cast<UserSearchModel*>(model_)) { UserSearchResult* userItem = static_cast<UserSearchResult*>(resultsPage_->results_->currentIndex().internalPointer()); if (userItem) { /* Remember to leave this if we change to dynamic cast */ @@ -198,17 +266,35 @@ JID QtUserSearchWindow::getContactJID() const { return jid; } +void QtUserSearchWindow::addSearchedJIDToList(const JID& jid) { + Contact contact(jid, jid.toString(), StatusShow::None, ""); + contactVector_.push_back(contact); + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->emitCompletenessCheck(); + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); + } +} + void QtUserSearchWindow::show() { clear(); QWidget::show(); } void QtUserSearchWindow::addSavedServices(const std::vector<JID>& services) { - firstPage_->service_->clear(); - foreach (JID jid, services) { - firstPage_->service_->addItem(P2QSTRING(jid.toString())); + if (type_ == AddContact) { + firstPage_->service_->clear(); + foreach (JID jid, services) { + firstPage_->service_->addItem(P2QSTRING(jid.toString())); + } + firstPage_->service_->clearEditText(); + } else { + firstMultiJIDPage_->service_->clear(); + foreach (JID jid, services) { + firstMultiJIDPage_->service_->addItem(P2QSTRING(jid.toString())); + } + firstMultiJIDPage_->service_->clearEditText(); } - firstPage_->service_->clearEditText(); } void QtUserSearchWindow::setSearchFields(boost::shared_ptr<SearchPayload> fields) { @@ -246,6 +332,66 @@ void QtUserSearchWindow::prepopulateJIDAndName(const JID& jid, const std::string detailsPage_->setName(name); } +void QtUserSearchWindow::setContactSuggestions(const std::vector<Contact>& suggestions) { + if (type_ == AddContact) { + firstPage_->jid_->setSuggestions(suggestions); + } else { + firstMultiJIDPage_->jid_->setSuggestions(suggestions); + } +} + +void QtUserSearchWindow::setJIDs(const std::vector<JID> &jids) { + foreach(JID jid, jids) { + addSearchedJIDToList(jid); + } + onJIDUpdateRequested(jids); +} + +void QtUserSearchWindow::setRoomJID(const JID& roomJID) { + roomJID_ = roomJID; +} + +std::string QtUserSearchWindow::getReason() const { + return Q2PSTRING(firstMultiJIDPage_->reason_->text()); +} + +std::vector<JID> QtUserSearchWindow::getJIDs() const { + std::vector<JID> jids; + foreach (const Contact& contact, contactVector_) { + jids.push_back(contact.jid); + } + return jids; +} + +void QtUserSearchWindow::setCanStartImpromptuChats(bool supportsImpromptu) { + supportsImpromptu_ = supportsImpromptu; + if (type_ == ChatToContact) { + firstMultiJIDPage_->contactList_->setMaximumNoOfContactsToOne(!supportsImpromptu_); + } +} + +void QtUserSearchWindow::updateContacts(const std::vector<Contact>& contacts) { + if (type_ != AddContact) { + firstMultiJIDPage_->contactList_->updateContacts(contacts); + } +} + +void QtUserSearchWindow::handleAddViaSearch() { + searchNext_ = true; + next(); +} + +void QtUserSearchWindow::handleListChanged(std::vector<Contact> list) { + contactVector_ = list; + if (type_ == ChatToContact) { + firstMultiJIDPage_->groupBox->setEnabled(supportsImpromptu_ ? 1 : (contactVector_.size() < 1)); + } +} + +void QtUserSearchWindow::handleJIDsAdded(std::vector<JID> jids) { + onJIDUpdateRequested(jids); +} + void QtUserSearchWindow::setResults(const std::vector<UserSearchResult>& results) { UserSearchModel *newModel = new UserSearchModel(); newModel->setResults(results); @@ -279,6 +425,60 @@ void QtUserSearchWindow::setSelectedService(const JID& jid) { myServer_ = jid; } +void QtUserSearchWindow::setFirstPage(QString title) { + if (page(1) != 0) { + removePage(1); + } + if (type_ == AddContact) { + firstPage_ = new QtUserSearchFirstPage(type_, title.isEmpty() ? firstPage_->title() : title, settings_); + connect(firstPage_->jid_, SIGNAL(textEdited(QString)), this, SLOT(handleContactSuggestionRequested(QString))); + connect(firstPage_->byJID_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); + connect(firstPage_->byLocalSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); + connect(firstPage_->byRemoteSearch_, SIGNAL(toggled(bool)), this, SLOT(handleFirstPageRadioChange())); +#if QT_VERSION >= 0x040700 + firstPage_->jid_->setPlaceholderText(tr("alice@wonderland.lit")); +#endif + firstPage_->service_->setEnabled(false); + setPage(1, firstPage_); + } else { + firstMultiJIDPage_ = new QtUserSearchFirstMultiJIDPage(type_, title.isEmpty() ? firstMultiJIDPage_->title() : title, settings_); + connect(firstMultiJIDPage_->addContactButton_, SIGNAL(clicked()), this, SLOT(addContact())); + connect(firstMultiJIDPage_->jid_, SIGNAL(textEdited(QString)), this, SLOT(handleContactSuggestionRequested(QString))); + connect(firstMultiJIDPage_->addViaSearchButton_, SIGNAL(clicked()), this, SLOT(handleAddViaSearch())); + connect(firstMultiJIDPage_->contactList_, SIGNAL(onListChanged(std::vector<Contact>)), this, SLOT(handleListChanged(std::vector<Contact>))); + connect(firstMultiJIDPage_->contactList_, SIGNAL(onJIDsAdded(std::vector<JID>)), this, SLOT(handleJIDsAdded(std::vector<JID>))); + setPage(1, firstMultiJIDPage_); + } +} + +void QtUserSearchWindow::setSecondPage() { + if (page(2) != 0) { + removePage(2); + } + fieldsPage_ = new QtUserSearchFieldsPage(); + fieldsPage_->fetchingThrobber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), this)); + fieldsPage_->fetchingThrobber_->movie()->stop(); + setPage(2, fieldsPage_); +} + +void QtUserSearchWindow::setThirdPage() { + if (page(3) != 0) { + removePage(3); + } + resultsPage_ = new QtUserSearchResultsPage(); + +#ifdef SWIFT_PLATFORM_MACOSX + resultsPage_->results_->setAlternatingRowColors(true); +#endif + if (type_ == AddContact) { + connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); + } + else { + connect(resultsPage_, SIGNAL(onUserTriggersContinue()), this, SLOT(next())); + } + setPage(3, resultsPage_); +} + void QtUserSearchWindow::clearForm() { fieldsPage_->fetchingThrobber_->show(); fieldsPage_->fetchingThrobber_->movie()->start(); @@ -294,32 +494,48 @@ void QtUserSearchWindow::clearForm() { } void QtUserSearchWindow::clear() { - firstPage_->errorLabel_->setVisible(false); QString howText; if (type_ == AddContact) { + firstPage_->errorLabel_->setVisible(false); howText = QString(tr("How would you like to find the user to add?")); + firstPage_->howLabel_->setText(howText); + firstPage_->byJID_->setChecked(true); + handleFirstPageRadioChange(); + } else { + contactVector_.clear(); + firstMultiJIDPage_->contactList_->setList(contactVector_); + firstMultiJIDPage_->errorLabel_->setVisible(false); + if (type_ == ChatToContact) { + howText = QString(tr("Who would you like to chat to?")); + } else if (type_ == InviteToChat) { + howText = QString(tr("Who do you want to invite to the chat?")); + } + firstMultiJIDPage_->howLabel_->setText(howText); } - else { - howText = QString(tr("How would you like to find the user to chat to?")); - } - firstPage_->howLabel_->setText(howText); - firstPage_->byJID_->setChecked(true); clearForm(); resultsPage_->results_->setModel(NULL); delete model_; model_ = NULL; - handleFirstPageRadioChange(); restart(); lastPage_ = 1; } void QtUserSearchWindow::setError(const QString& error) { if (error.isEmpty()) { - firstPage_->errorLabel_->hide(); + if (type_ == AddContact) { + firstPage_->errorLabel_->hide(); + } else { + firstMultiJIDPage_->errorLabel_->hide(); + } } else { - firstPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); - firstPage_->errorLabel_->show(); + if (type_ == AddContact) { + firstPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); + firstPage_->errorLabel_->show(); + } else { + firstMultiJIDPage_->errorLabel_->setText(QString("<font color='red'>%1</font>").arg(error)); + firstMultiJIDPage_->errorLabel_->show(); + } restart(); lastPage_ = 1; } diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h index 32e851a..e5a9f80 100644 --- a/Swift/QtUI/UserSearch/QtUserSearchWindow.h +++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.h @@ -18,15 +18,17 @@ namespace Swift { class UserSearchResult; class UIEventStream; class QtUserSearchFirstPage; + class QtUserSearchFirstMultiJIDPage; class QtUserSearchFieldsPage; class QtUserSearchResultsPage; class QtUserSearchDetailsPage; class QtFormResultItemModel; + class SettingsProvider; class QtUserSearchWindow : public QWizard, public UserSearchWindow, private Ui::QtUserSearchWizard { Q_OBJECT public: - QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups); + QtUserSearchWindow(UIEventStream* eventStream, UserSearchWindow::Type type, const std::set<std::string>& groups, SettingsProvider* settingsProvider); virtual ~QtUserSearchWindow(); virtual void addSavedServices(const std::vector<JID>& services); @@ -41,19 +43,39 @@ namespace Swift { virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields); virtual void setNameSuggestions(const std::vector<std::string>& suggestions); virtual void prepopulateJIDAndName(const JID& jid, const std::string& name); + virtual void setContactSuggestions(const std::vector<Contact>& suggestions); + virtual void setJIDs(const std::vector<JID> &jids); + virtual void setRoomJID(const JID &roomJID); + virtual std::string getReason() const; + virtual std::vector<JID> getJIDs() const; + virtual void setCanStartImpromptuChats(bool supportsImpromptu); + virtual void updateContacts(const std::vector<Contact> &contacts); protected: virtual int nextId() const; + private slots: void handleFirstPageRadioChange(); virtual void handleCurrentChanged(int); virtual void handleAccepted(); + void handleContactSuggestionRequested(const QString& text); + void addContact(); + void handleAddViaSearch(); + void handleListChanged(std::vector<Contact> list); + void handleJIDsAdded(std::vector<JID> jids); + + private: + void setFirstPage(QString title = ""); + void setSecondPage(); + void setThirdPage(); + private: void clearForm(); void setError(const QString& error); JID getServerToSearch(); void handleSearch(); JID getContactJID() const; + void addSearchedJIDToList(const JID& jid); private: UIEventStream* eventStream_; @@ -61,10 +83,16 @@ namespace Swift { QAbstractItemModel* model_; UserSearchDelegate* delegate_; QtUserSearchFirstPage* firstPage_; + QtUserSearchFirstMultiJIDPage* firstMultiJIDPage_; QtUserSearchFieldsPage* fieldsPage_; QtUserSearchResultsPage* resultsPage_; QtUserSearchDetailsPage* detailsPage_; JID myServer_; + JID roomJID_; int lastPage_; + std::vector<Contact> contactVector_; + SettingsProvider* settings_; + bool searchNext_; + bool supportsImpromptu_; }; } |