/* * Squawk messenger. * Copyright (C) 2019 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "squawk.h" #include "ui_squawk.h" #include #include Squawk::Squawk(Models::Roster& p_rosterModel, QWidget *parent) : QMainWindow(parent), m_ui(new Ui::Squawk), accounts(nullptr), preferences(nullptr), about(nullptr), rosterModel(p_rosterModel), contextMenu(new QMenu()), infoWidgets(), currentConversation(nullptr), restoreSelection(), needToRestore(false) { m_ui->setupUi(this); m_ui->roster->setModel(&rosterModel); m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu); if (QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1) m_ui->roster->setColumnWidth(1, 52); else m_ui->roster->setColumnWidth(1, 26); m_ui->roster->setIconSize(QSize(20, 20)); m_ui->roster->header()->setStretchLastSection(false); m_ui->roster->header()->setSectionResizeMode(0, QHeaderView::Stretch); for (int i = static_cast(Shared::AvailabilityLowest); i < static_cast(Shared::AvailabilityHighest) + 1; ++i) { Shared::Availability av = static_cast(i); m_ui->comboBox->addItem(Shared::availabilityIcon(av), Shared::Global::getName(av)); } m_ui->comboBox->setCurrentIndex(static_cast(Shared::Availability::offline)); connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts); connect(m_ui->actionPreferences, &QAction::triggered, this, &Squawk::onPreferences); connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact); connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference); connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::quit); connect(m_ui->comboBox, qOverload(&QComboBox::activated), this, &Squawk::onComboboxActivated); //connect(m_ui->roster, &QTreeView::doubleClicked, this, &Squawk::onRosterItemDoubleClicked); connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu); connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::itemCollapsed); connect(m_ui->roster, &QTreeView::expanded, this, &Squawk::itemExpanded); connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged); connect(rosterModel.accountsModel, &Models::Accounts::changed, this, &Squawk::onAccountsChanged); connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide); connect(m_ui->actionAboutSquawk, &QAction::triggered, this, &Squawk::onAboutSquawkCalled); //m_ui->mainToolBar->addWidget(m_ui->comboBox); if (testAttribute(Qt::WA_TranslucentBackground)) m_ui->roster->viewport()->setAutoFillBackground(false); QSettings settings; settings.beginGroup("ui"); settings.beginGroup("window"); if (settings.contains("geometry")) restoreGeometry(settings.value("geometry").toByteArray()); if (settings.contains("state")) restoreState(settings.value("state").toByteArray()); settings.endGroup(); if (settings.contains("splitter")) m_ui->splitter->restoreState(settings.value("splitter").toByteArray()); settings.endGroup(); onAccountsChanged(); } Squawk::~Squawk() { delete contextMenu; } void Squawk::onAccounts() { if (accounts == nullptr) { accounts = new Accounts(rosterModel.accountsModel); accounts->setAttribute(Qt::WA_DeleteOnClose); connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed); connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest); connect(accounts, &Accounts::changeAccount, this, &Squawk::modifyAccountRequest); connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount); connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount); connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest); accounts->show(); } else { accounts->show(); accounts->raise(); accounts->activateWindow(); } } void Squawk::onPreferences() { if (preferences == nullptr) { preferences = new Settings(); preferences->setAttribute(Qt::WA_DeleteOnClose); connect(preferences, &Settings::destroyed, this, &Squawk::onPreferencesClosed); connect(preferences, &Settings::changeDownloadsPath, this, &Squawk::changeDownloadsPath); connect(preferences, &Settings::changeTray, this, &Squawk::changeTray); preferences->show(); } else { preferences->show(); preferences->raise(); preferences->activateWindow(); } } void Squawk::onAccountsChanged() { unsigned int size = rosterModel.accountsModel->activeSize(); if (size > 0) { m_ui->actionAddContact->setEnabled(true); m_ui->actionAddConference->setEnabled(true); } else { m_ui->actionAddContact->setEnabled(false); m_ui->actionAddConference->setEnabled(false); } } void Squawk::onNewContact() { NewContact* nc = new NewContact(rosterModel.accountsModel, this); connect(nc, &NewContact::accepted, this, &Squawk::onNewContactAccepted); connect(nc, &NewContact::rejected, nc, &NewContact::deleteLater); nc->exec(); } void Squawk::onNewConference() { JoinConference* jc = new JoinConference(rosterModel.accountsModel, this); connect(jc, &JoinConference::accepted, this, &Squawk::onJoinConferenceAccepted); connect(jc, &JoinConference::rejected, jc, &JoinConference::deleteLater); jc->exec(); } void Squawk::onNewContactAccepted() { NewContact* nc = static_cast(sender()); NewContact::Data value = nc->value(); emit addContactRequest(value.account, value.jid, value.name, value.groups); nc->deleteLater(); } void Squawk::onJoinConferenceAccepted() { JoinConference* jc = static_cast(sender()); JoinConference::Data value = jc->value(); emit addRoomRequest(value.account, value.jid, value.nick, value.password, value.autoJoin); jc->deleteLater(); } void Squawk::closeEvent(QCloseEvent* event) { if (accounts != nullptr) accounts->close(); if (preferences != nullptr) preferences->close(); if (about != nullptr) about->close(); for (const std::pair& pair : infoWidgets) { disconnect(pair.second, &UI::Info::destroyed, this, &Squawk::onInfoClosed); pair.second->close(); } infoWidgets.clear(); writeSettings(); emit closing();; QMainWindow::closeEvent(event); } void Squawk::onAccountsClosed() { accounts = nullptr;} void Squawk::onPreferencesClosed() { preferences = nullptr;} void Squawk::onAboutSquawkClosed() { about = nullptr;} void Squawk::onComboboxActivated(int index) { Shared::Availability av = Shared::Global::fromInt(index); emit changeState(av); } void Squawk::expand(const QModelIndex& index) { m_ui->roster->expand(index);} void Squawk::stateChanged(Shared::Availability state) { m_ui->comboBox->setCurrentIndex(static_cast(state));} void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) { if (item.isValid()) { Models::Item* node = static_cast(item.internalPointer()); if (node->type == Models::Item::reference) node = static_cast(node)->dereference(); Models::Contact* contact = nullptr; Models::Room* room = nullptr; switch (node->type) { case Models::Item::contact: contact = static_cast(node); emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid())); break; case Models::Item::presence: contact = static_cast(node->parentItem()); emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()), node->getName()); break; case Models::Item::room: room = static_cast(node); emit openConversation(Models::Roster::ElId(room->getAccountName(), room->getJid())); break; default: m_ui->roster->expand(item); break; } } } void Squawk::closeCurrentConversation() { if (currentConversation != nullptr) { currentConversation->deleteLater(); currentConversation = nullptr; m_ui->filler->show(); } } void Squawk::onRosterContextMenu(const QPoint& point) { QModelIndex index = m_ui->roster->indexAt(point); if (index.isValid()) { Models::Item* item = static_cast(index.internalPointer()); if (item->type == Models::Item::reference) item = static_cast(item)->dereference(); contextMenu->clear(); bool hasMenu = false; bool active = item->getAccountConnectionState() == Shared::ConnectionState::connected; switch (item->type) { case Models::Item::account: { Models::Account* acc = static_cast(item); hasMenu = true; QString name = acc->getName(); if (acc->getActive()) { QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Deactivate")); connect(con, &QAction::triggered, std::bind(&Squawk::disconnectAccount, this, name)); } else { QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Activate")); connect(con, &QAction::triggered, std::bind(&Squawk::connectAccount, this, name)); } QAction* info = contextMenu->addAction(Shared::icon("user-properties"), tr("Info")); info->setEnabled(active); connect(info, &QAction::triggered, std::bind(&Squawk::onActivateInfo, this, name, acc->getBareJid())); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); connect(remove, &QAction::triggered, std::bind(&Squawk::removeAccountRequest, this, name)); } break; case Models::Item::contact: { Models::Contact* cnt = static_cast(item); Models::Roster::ElId id(cnt->getAccountName(), cnt->getJid()); QString cntName = cnt->getName(); hasMenu = true; QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog")); dialog->setEnabled(active); connect(dialog, &QAction::triggered, std::bind(&Squawk::onRosterItemDoubleClicked, this, index)); Shared::SubscriptionState state = cnt->getState(); switch (state) { case Shared::SubscriptionState::both: case Shared::SubscriptionState::to: { QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); unsub->setEnabled(active); connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false)); } break; case Shared::SubscriptionState::from: case Shared::SubscriptionState::unknown: case Shared::SubscriptionState::none: { QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); sub->setEnabled(active); connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true)); } } QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename")); rename->setEnabled(active); connect(rename, &QAction::triggered, [this, cntName, id]() { QInputDialog* dialog = new QInputDialog(this); connect(dialog, &QDialog::accepted, [this, dialog, cntName, id]() { QString newName = dialog->textValue(); if (newName != cntName) emit renameContactRequest(id.account, id.name, newName); dialog->deleteLater(); }); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); dialog->setInputMode(QInputDialog::TextInput); dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(id.name)); dialog->setWindowTitle(tr("Renaming %1").arg(id.name)); dialog->setTextValue(cntName); dialog->exec(); }); QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups")); std::deque groupList = rosterModel.groupList(id.account); for (QString groupName : groupList) { QAction* gr = groupsMenu->addAction(groupName); gr->setCheckable(true); gr->setChecked(rosterModel.groupHasContact(id.account, groupName, id.name)); gr->setEnabled(active); connect(gr, &QAction::toggled, [this, groupName, id](bool checked) { if (checked) emit addContactToGroupRequest(id.account, id.name, groupName); else emit removeContactFromGroupRequest(id.account, id.name, groupName); }); } QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group")); newGroup->setEnabled(active); connect(newGroup, &QAction::triggered, [this, id]() { QInputDialog* dialog = new QInputDialog(this); connect(dialog, &QDialog::accepted, [this, dialog, id]() { emit addContactToGroupRequest(id.account, id.name, dialog->textValue()); dialog->deleteLater(); }); connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater); dialog->setInputMode(QInputDialog::TextInput); dialog->setLabelText(tr("New group name")); dialog->setWindowTitle(tr("Add %1 to a new group").arg(id.name)); dialog->exec(); }); QAction* info = contextMenu->addAction(Shared::icon("user-properties"), tr("Info")); info->setEnabled(active); connect(info, &QAction::triggered, std::bind(&Squawk::onActivateInfo, this, id.account, id.name)); QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); remove->setEnabled(active); connect(remove, &QAction::triggered, std::bind(&Squawk::removeContactRequest, this, id.account, id.name)); } break; case Models::Item::room: { Models::Room* room = static_cast(item); hasMenu = true; QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open conversation")); dialog->setEnabled(active); connect(dialog, &QAction::triggered, [this, index]() { onRosterItemDoubleClicked(index); }); Models::Roster::ElId id(room->getAccountName(), room->getJid()); if (room->getAutoJoin()) { QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe")); unsub->setEnabled(active); connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false)); } else { QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe")); sub->setEnabled(active); connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true)); } QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove")); remove->setEnabled(active); connect(remove, &QAction::triggered, std::bind(&Squawk::removeRoomRequest, this, id.account, id.name)); } break; default: break; } if (hasMenu) contextMenu->popup(m_ui->roster->viewport()->mapToGlobal(point)); } } void Squawk::responseInfo(const Shared::Info& info) { std::map::const_iterator itr = infoWidgets.find(info.getAddressRef()); if (itr != infoWidgets.end()) { itr->second->setData(info); itr->second->hideProgress(); } } void Squawk::onInfoClosed() { UI::Info* info = static_cast(sender()); std::map::const_iterator itr = infoWidgets.find(info->getJid()); if (itr == infoWidgets.end()) { qDebug() << "Info widget has been closed but can not be found among other opened vCards, application is most probably going to crash"; return; } infoWidgets.erase(itr); } void Squawk::onActivateInfo(const QString& account, const QString& jid) { std::map::const_iterator itr = infoWidgets.find(jid); UI::Info* info; if (itr != infoWidgets.end()) { info = itr->second; } else { info = new UI::Info(jid); info->setWindowTitle(tr("Information about %1").arg(jid)); info->setAttribute(Qt::WA_DeleteOnClose); infoWidgets.insert(std::make_pair(jid, info)); connect(info, &UI::Info::destroyed, this, &Squawk::onInfoClosed); connect(info, &UI::Info::saveInfo, std::bind(&Squawk::onInfoSave, this, std::placeholders::_1, account)); } info->show(); info->raise(); info->activateWindow(); info->showProgress(); emit requestInfo(account, jid); } void Squawk::onInfoSave(const Shared::Info& info, const QString& account) { UI::Info* widget = static_cast(sender()); emit updateInfo(account, info); widget->deleteLater(); } void Squawk::writeSettings() { QSettings settings; settings.beginGroup("ui"); settings.beginGroup("window"); settings.setValue("geometry", saveGeometry()); settings.setValue("state", saveState()); settings.endGroup(); settings.setValue("splitter", m_ui->splitter->saveState()); settings.endGroup(); } void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous) { if (restoreSelection.isValid() && restoreSelection == current) { restoreSelection = QModelIndex(); return; } if (current.isValid()) { Models::Item* node = static_cast(current.internalPointer()); if (node->type == Models::Item::reference) node = static_cast(node)->dereference(); Models::Contact* contact = nullptr; Models::Room* room = nullptr; QString res; Models::Roster::ElId* id = nullptr; bool hasContext = true; switch (node->type) { case Models::Item::contact: contact = static_cast(node); id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); break; case Models::Item::presence: contact = static_cast(node->parentItem()); id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid()); res = node->getName(); hasContext = false; break; case Models::Item::room: room = static_cast(node); id = new Models::Roster::ElId(room->getAccountName(), room->getJid()); break; case Models::Item::participant: room = static_cast(node->parentItem()); id = new Models::Roster::ElId(room->getAccountName(), room->getJid()); hasContext = false; break; case Models::Item::group: hasContext = false; default: break; } if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) { if (id != nullptr) delete id; needToRestore = true; restoreSelection = previous; return; } if (id != nullptr) { if (currentConversation != nullptr) { if (currentConversation->getId() == *id) { if (contact != nullptr) currentConversation->setPalResource(res); return; } else { currentConversation->deleteLater(); } } else { m_ui->filler->hide(); } Models::Account* acc = rosterModel.getAccount(id->account); if (contact != nullptr) currentConversation = new Chat(acc, contact); else if (room != nullptr) currentConversation = new Room(acc, room); if (!testAttribute(Qt::WA_TranslucentBackground)) currentConversation->setFeedFrames(true, false, true, true); emit openedConversation(); if (res.size() > 0) currentConversation->setPalResource(res); m_ui->splitter->insertWidget(1, currentConversation); delete id; } else { closeCurrentConversation(); } } else { closeCurrentConversation(); } } void Squawk::onContextAboutToHide() { if (needToRestore) { needToRestore = false; m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect); } } void Squawk::onAboutSquawkCalled() { if (about == nullptr) { about = new About(); about->setAttribute(Qt::WA_DeleteOnClose); connect(about, &Settings::destroyed, this, &Squawk::onAboutSquawkClosed); } else { about->raise(); about->activateWindow(); } about->show(); } Models::Roster::ElId Squawk::currentConversationId() const { if (currentConversation == nullptr) return Models::Roster::ElId(); else return Models::Roster::ElId(currentConversation->getAccount(), currentConversation->getJid()); } void Squawk::select(QModelIndex index) { m_ui->roster->scrollTo(index, QAbstractItemView::EnsureVisible); m_ui->roster->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); }