1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/src/widget/form/profileform.cpp

575 lines
20 KiB
C++

/*
Copyright © 2014-2018 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre 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.
qTox 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 qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "profileform.h"
#include "ui_profileform.h"
#include "src/core/core.h"
#include "src/model/profile/iprofileinfo.h"
#include "src/net/toxme.h"
#include "src/persistence/profile.h"
#include "src/persistence/profilelocker.h"
#include "src/persistence/settings.h"
#include "src/widget/contentlayout.h"
#include "src/widget/form/setpassworddialog.h"
#include "src/widget/form/settingswidget.h"
#include "src/widget/gui.h"
#include "src/widget/maskablepixmapwidget.h"
#include "src/widget/style.h"
#include "src/widget/tool/croppinglabel.h"
#include "src/widget/translator.h"
#include "src/widget/widget.h"
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QComboBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QImageReader>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMap>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QWindow>
static const QMap<IProfileInfo::SetAvatarResult, QString> SET_AVATAR_ERROR = {
{ IProfileInfo::SetAvatarResult::CanNotOpen,
ProfileForm::tr("Unable to open this file.") },
{ IProfileInfo::SetAvatarResult::CanNotRead,
ProfileForm::tr("Unable to read this image.") },
{ IProfileInfo::SetAvatarResult::TooLarge,
ProfileForm::tr("The supplied image is too large.\nPlease use another image.") },
{ IProfileInfo::SetAvatarResult::EmptyPath,
ProfileForm::tr("Empty path is unavaliable") },
};
static const QMap<IProfileInfo::RenameResult, QPair<QString, QString>> RENAME_ERROR = {
{ IProfileInfo::RenameResult::Error,
{ ProfileForm::tr("Failed to rename"),
ProfileForm::tr("Couldn't rename the profile to \"%1\"") }
} ,
{ IProfileInfo::RenameResult::ProfileAlreadyExists,
{ ProfileForm::tr("Profile already exists"),
ProfileForm::tr("A profile named \"%1\" already exists.") }
},
{ IProfileInfo::RenameResult::EmptyName,
{ ProfileForm::tr("Empty name"),
ProfileForm::tr("Empty name is unavaliable") }
},
};
static const QMap<IProfileInfo::SaveResult, QPair<QString, QString>> SAVE_ERROR = {
{ IProfileInfo::SaveResult::NoWritePermission,
{ ProfileForm::tr("Location not writable", "Title of permissions popup"),
ProfileForm::tr("You do not have permission to write that location. Choose "
"another, or cancel the save dialog.", "text of permissions popup") },
},
{ IProfileInfo::SaveResult::Error,
{ ProfileForm::tr("Failed to copy file"),
ProfileForm::tr("The file you chose could not be written to.") }
},
{ IProfileInfo::SaveResult::EmptyPath,
{ ProfileForm::tr("Empty path"),
ProfileForm::tr("Empty path is unavaliable") }
},
};
static const QPair<QString, QString> CAN_NOT_CHANGE_PASSWORD = {
ProfileForm::tr("Couldn't change password"),
ProfileForm::tr("Couldn't change password on the database, "
"it might be corrupted or use the old password.")
};
ProfileForm::ProfileForm(IProfileInfo* profileInfo, QWidget* parent)
: QWidget{parent}
, qr{nullptr}
, profileInfo{profileInfo}
{
bodyUI = new Ui::IdentitySettings;
bodyUI->setupUi(this);
const uint32_t maxNameLength = tox_max_name_length();
const QString toolTip = tr("Tox user names cannot exceed %1 characters.").arg(maxNameLength);
bodyUI->userNameLabel->setToolTip(toolTip);
bodyUI->userName->setMaxLength(static_cast<int>(maxNameLength));
// tox
toxId = new ClickableTE();
toxId->setFont(Style::getFont(Style::Small));
toxId->setToolTip(bodyUI->toxId->toolTip());
QVBoxLayout* toxIdGroup = qobject_cast<QVBoxLayout*>(bodyUI->toxGroup->layout());
delete toxIdGroup->replaceWidget(bodyUI->toxId, toxId); // Original toxId is in heap, delete it
bodyUI->toxId->hide();
/* Toxme section init */
bodyUI->toxmeServersList->addItem("toxme.io");
QString toxmeInfo = Settings::getInstance().getToxmeInfo();
// User not registered
if (toxmeInfo.isEmpty()) {
showRegisterToxme();
} else {
showExistingToxme();
}
bodyUI->qrLabel->setWordWrap(true);
QRegExp re("[^@ ]+");
QRegExpValidator* validator = new QRegExpValidator(re, this);
bodyUI->toxmeUsername->setValidator(validator);
profilePicture = new MaskablePixmapWidget(this, QSize(64, 64), ":/img/avatar_mask.svg");
profilePicture->setPixmap(QPixmap(":/img/contact_dark.svg"));
profilePicture->setContextMenuPolicy(Qt::CustomContextMenu);
profilePicture->setClickable(true);
profilePicture->setObjectName("selfAvatar");
profilePicture->installEventFilter(this);
profilePicture->setAccessibleName("Profile avatar");
profilePicture->setAccessibleDescription("Set a profile avatar shown to all contacts");
profilePicture->setStyleSheet(Style::getStylesheet("window/profile.css"));
connect(profilePicture, &MaskablePixmapWidget::clicked, this, &ProfileForm::onAvatarClicked);
connect(profilePicture, &MaskablePixmapWidget::customContextMenuRequested,
this, &ProfileForm::showProfilePictureContextMenu);
QHBoxLayout* publicGrouplayout = qobject_cast<QHBoxLayout*>(bodyUI->publicGroup->layout());
publicGrouplayout->insertWidget(0, profilePicture);
publicGrouplayout->insertSpacing(1, 7);
timer.setInterval(750);
timer.setSingleShot(true);
connect(&timer, &QTimer::timeout, this, [=]() {
bodyUI->toxIdLabel->setText(bodyUI->toxIdLabel->text().replace("", ""));
hasCheck = false;
});
connect(bodyUI->toxIdLabel, &CroppingLabel::clicked, this, &ProfileForm::copyIdClicked);
connect(toxId, &ClickableTE::clicked, this, &ProfileForm::copyIdClicked);
connect(profileInfo, &IProfileInfo::idChanged, this, &ProfileForm::setToxId);
connect(bodyUI->userName, &QLineEdit::editingFinished, this, &ProfileForm::onUserNameEdited);
connect(bodyUI->statusMessage, &QLineEdit::editingFinished,
this, &ProfileForm::onStatusMessageEdited);
connect(bodyUI->renameButton, &QPushButton::clicked, this, &ProfileForm::onRenameClicked);
connect(bodyUI->exportButton, &QPushButton::clicked, this, &ProfileForm::onExportClicked);
connect(bodyUI->deleteButton, &QPushButton::clicked, this, &ProfileForm::onDeleteClicked);
connect(bodyUI->logoutButton, &QPushButton::clicked, this, &ProfileForm::onLogoutClicked);
connect(bodyUI->deletePassButton, &QPushButton::clicked,
this, &ProfileForm::onDeletePassClicked);
connect(bodyUI->changePassButton, &QPushButton::clicked,
this, &ProfileForm::onChangePassClicked);
connect(bodyUI->deletePassButton, &QPushButton::clicked,
this, &ProfileForm::setPasswordButtonsText);
connect(bodyUI->changePassButton, &QPushButton::clicked,
this, &ProfileForm::setPasswordButtonsText);
connect(bodyUI->saveQr, &QPushButton::clicked, this, &ProfileForm::onSaveQrClicked);
connect(bodyUI->copyQr, &QPushButton::clicked, this, &ProfileForm::onCopyQrClicked);
connect(bodyUI->toxmeRegisterButton, &QPushButton::clicked,
this, &ProfileForm::onRegisterButtonClicked);
connect(bodyUI->toxmeUpdateButton, &QPushButton::clicked,
this, &ProfileForm::onRegisterButtonClicked);
connect(profileInfo, &IProfileInfo::usernameChanged, this,
[=](const QString& val) { bodyUI->userName->setText(val); });
connect(profileInfo, &IProfileInfo::statusMessageChanged, this,
[=](const QString& val) { bodyUI->statusMessage->setText(val); });
for (QComboBox* cb : findChildren<QComboBox*>()) {
cb->installEventFilter(this);
cb->setFocusPolicy(Qt::StrongFocus);
}
retranslateUi();
Translator::registerHandler(std::bind(&ProfileForm::retranslateUi, this), this);
}
void ProfileForm::prFileLabelUpdate()
{
const QString name = profileInfo->getProfileName();
bodyUI->prFileLabel->setText(tr("Current profile: ") + name + ".tox");
}
ProfileForm::~ProfileForm()
{
Translator::unregister(this);
delete qr;
delete bodyUI;
}
bool ProfileForm::isShown() const
{
if (profilePicture->isVisible()) {
window()->windowHandle()->alert(0);
return true;
}
return false;
}
void ProfileForm::show(ContentLayout* contentLayout)
{
contentLayout->mainContent->layout()->addWidget(this);
QWidget::show();
prFileLabelUpdate();
bool portable = Settings::getInstance().getMakeToxPortable();
QString defaultPath = QDir(Settings::getInstance().getSettingsDirPath()).path().trimmed();
QString appPath = QApplication::applicationDirPath();
QString dirPath = portable ? appPath : defaultPath;
QString dirPrLink =
tr("Current profile location: %1").arg(QString("<a href=\"file://%1\">%1</a>").arg(dirPath));
bodyUI->dirPrLink->setText(dirPrLink);
bodyUI->dirPrLink->setOpenExternalLinks(true);
bodyUI->dirPrLink->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::TextSelectableByMouse);
bodyUI->dirPrLink->setMaximumSize(bodyUI->dirPrLink->sizeHint());
bodyUI->userName->setFocus();
bodyUI->userName->selectAll();
}
bool ProfileForm::eventFilter(QObject* object, QEvent* event)
{
if (object == static_cast<QObject*>(profilePicture) && event->type() == QEvent::MouseButtonPress) {
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::RightButton)
return true;
}
return false;
}
void ProfileForm::showProfilePictureContextMenu(const QPoint& point)
{
const QPoint pos = profilePicture->mapToGlobal(point);
QMenu contextMenu;
const QIcon icon = style()->standardIcon(QStyle::SP_DialogCancelButton);
const QAction* removeAction = contextMenu.addAction(icon, tr("Remove"));
const QAction* selectedItem = contextMenu.exec(pos);
if (selectedItem == removeAction) {
profileInfo->removeAvatar();
}
}
void ProfileForm::copyIdClicked()
{
profileInfo->copyId();
if (!hasCheck) {
bodyUI->toxIdLabel->setText(bodyUI->toxIdLabel->text() + "");
hasCheck = true;
}
timer.start();
}
void ProfileForm::onUserNameEdited()
{
profileInfo->setUsername(bodyUI->userName->text());
}
void ProfileForm::onStatusMessageEdited()
{
profileInfo->setStatusMessage(bodyUI->statusMessage->text());
}
void ProfileForm::onSelfAvatarLoaded(const QPixmap& pic)
{
profilePicture->setPixmap(pic);
}
void ProfileForm::setToxId(const ToxId& id)
{
QString idString = id.toString();
static const QString ToxIdColor = QStringLiteral("%1"
"<span style='color:blue'>%2</span>"
"<span style='color:gray'>%3</span>");
toxId->setText(ToxIdColor
.arg(idString.mid(0, 64))
.arg(idString.mid(64, 8))
.arg(idString.mid(72, 4)));
delete qr;
qr = new QRWidget();
qr->setQRData("tox:" + idString);
bodyUI->qrCode->setPixmap(QPixmap::fromImage(qr->getImage()->scaledToWidth(150)));
}
QString ProfileForm::getSupportedImageFilter()
{
QString res;
for (auto type : QImageReader::supportedImageFormats()) {
res += QString("*.%1 ").arg(QString(type));
}
return tr("Images (%1)", "filetype filter").arg(res.left(res.size() - 1));
}
void ProfileForm::onAvatarClicked()
{
const QString filter = getSupportedImageFilter();
const QString path = QFileDialog::getOpenFileName(Q_NULLPTR, tr("Choose a profile picture"),
QDir::homePath(), filter, nullptr);
if (path.isEmpty()) {
return;
}
const IProfileInfo::SetAvatarResult result = profileInfo->setAvatar(path);
if (result == IProfileInfo::SetAvatarResult::OK) {
return;
}
GUI::showError(tr("Error"), SET_AVATAR_ERROR[result]);
}
void ProfileForm::onRenameClicked()
{
const QString cur = profileInfo->getProfileName();
const QString title = tr("Rename \"%1\"", "renaming a profile").arg(cur);
const QString name = QInputDialog::getText(this, title, title + ":");
if (name.isEmpty()) {
return;
}
const IProfileInfo::RenameResult result = profileInfo->renameProfile(name);
if (result == IProfileInfo::RenameResult::OK) {
return;
}
const QPair<QString, QString> error = RENAME_ERROR[result];
GUI::showError(error.first, error.second.arg(name));
prFileLabelUpdate();
}
void ProfileForm::onExportClicked()
{
const QString current = profileInfo->getProfileName() + Core::TOX_EXT;
//:save dialog title
const QString path = QFileDialog::getSaveFileName(Q_NULLPTR, tr("Export profile"), current,
//: save dialog filter
tr("Tox save file (*.tox)"));
if (path.isEmpty()) {
return;
}
const IProfileInfo::SaveResult result = profileInfo->exportProfile(path);
if (result == IProfileInfo::SaveResult::OK) {
return;
}
const QPair<QString, QString> error = SAVE_ERROR[result];
GUI::showWarning(error.first, error.second);
}
void ProfileForm::onDeleteClicked()
{
const QString title = tr("Really delete profile?", "deletion confirmation title");
const QString question = tr("Are you sure you want to delete this profile?",
"deletion confirmation text");
if (!GUI::askQuestion(title, question)) {
return;
}
const QStringList manualDeleteFiles = profileInfo->removeProfile();
if (manualDeleteFiles.empty()) {
return;
}
//: deletion failed text part 1
QString message = tr("The following files could not be deleted:") + "\n\n";
for (const QString& file : manualDeleteFiles) {
message += file + "\n";
}
//: deletion failed text part 2
message += "\n" + tr("Please manually remove them.");
GUI::showError(tr("Files could not be deleted!", "deletion failed title"), message);
}
void ProfileForm::onLogoutClicked()
{
profileInfo->logout();
}
void ProfileForm::setPasswordButtonsText()
{
if (profileInfo->isEncrypted()) {
bodyUI->changePassButton->setText(tr("Change password", "button text"));
bodyUI->deletePassButton->setVisible(true);
} else {
bodyUI->changePassButton->setText(tr("Set profile password", "button text"));
bodyUI->deletePassButton->setVisible(false);
}
}
void ProfileForm::onCopyQrClicked()
{
profileInfo->copyQr(*qr->getImage());
}
void ProfileForm::onSaveQrClicked()
{
const QString current = profileInfo->getProfileName() + ".png";
const QString path = QFileDialog::getSaveFileName(
Q_NULLPTR, tr("Save", "save qr image"), current,
tr("Save QrCode (*.png)", "save dialog filter"));
if (path.isEmpty()) {
return;
}
const IProfileInfo::SaveResult result = profileInfo->saveQr(*qr->getImage(), path);
if (result == IProfileInfo::SaveResult::OK) {
return;
}
const QPair<QString, QString> error = SAVE_ERROR[result];
GUI::showWarning(error.first, error.second);
}
void ProfileForm::onDeletePassClicked()
{
if (!profileInfo->isEncrypted()) {
GUI::showInfo(tr("Nothing to remove"), tr("Your profile does not have a password!"));
return;
}
const QString title = tr("Really delete password?", "deletion confirmation title");
//: deletion confirmation text
const QString body = tr("Are you sure you want to delete your password?");
if (!GUI::askQuestion(title, body)) {
return;
}
if (!profileInfo->deletePassword()) {
GUI::showInfo(CAN_NOT_CHANGE_PASSWORD.first, CAN_NOT_CHANGE_PASSWORD.second);
}
}
void ProfileForm::onChangePassClicked()
{
const QString title = tr("Please enter a new password.");
SetPasswordDialog* dialog = new SetPasswordDialog(title, QString{}, nullptr);
if (dialog->exec() == QDialog::Rejected) {
return;
}
QString newPass = dialog->getPassword();
if (!profileInfo->setPassword(newPass)) {
GUI::showInfo(CAN_NOT_CHANGE_PASSWORD.first, CAN_NOT_CHANGE_PASSWORD.second);
}
}
void ProfileForm::retranslateUi()
{
bodyUI->retranslateUi(this);
setPasswordButtonsText();
// We have to add the toxId tooltip here and not in the .ui or Qt won't know how to translate it
// dynamically
toxId->setToolTip(tr("This bunch of characters tells other Tox clients how to contact "
"you.\nShare it with your friends to communicate.\n\n"
"This ID includes the NoSpam code (in blue), and the checksum (in gray)."));
}
void ProfileForm::showRegisterToxme()
{
bodyUI->toxmeUsername->setText("");
bodyUI->toxmeBio->setText("");
bodyUI->toxmePrivacy->setChecked(false);
bodyUI->toxmeRegisterButton->show();
bodyUI->toxmeUpdateButton->hide();
bodyUI->toxmePassword->hide();
bodyUI->toxmePasswordLabel->hide();
}
void ProfileForm::showExistingToxme()
{
QStringList info = Settings::getInstance().getToxmeInfo().split("@");
bodyUI->toxmeUsername->setText(info[0]);
bodyUI->toxmeServersList->addItem(info[1]);
QString bio = Settings::getInstance().getToxmeBio();
bodyUI->toxmeBio->setText(bio);
bool priv = Settings::getInstance().getToxmePriv();
bodyUI->toxmePrivacy->setChecked(priv);
QString pass = Settings::getInstance().getToxmePass();
bodyUI->toxmePassword->setText(pass);
bodyUI->toxmePassword->show();
bodyUI->toxmePasswordLabel->show();
bodyUI->toxmeRegisterButton->hide();
bodyUI->toxmeUpdateButton->show();
}
void ProfileForm::onRegisterButtonClicked()
{
QString name = bodyUI->toxmeUsername->text();
if (name.isEmpty()) {
return;
}
bodyUI->toxmeRegisterButton->setEnabled(false);
bodyUI->toxmeUpdateButton->setEnabled(false);
bodyUI->toxmeRegisterButton->setText(tr("Register (processing)"));
bodyUI->toxmeUpdateButton->setText(tr("Update (processing)"));
QString id = toxId->text();
id.remove(QRegularExpression("<[^>]*>"));
QString bio = bodyUI->toxmeBio->text();
QString server = bodyUI->toxmeServersList->currentText();
bool privacy = bodyUI->toxmePrivacy->isChecked();
Core* oldCore = Core::getInstance();
ToxmeData::ExecCode code = ToxmeData::ExecCode::Ok;
QString response = Toxme::createAddress(code, server, ToxId(id), name, privacy, bio);
Core* newCore = Core::getInstance();
// Make sure the user didn't logout (or logout and login)
// before the request is finished, else qTox will crash.
if (oldCore == newCore) {
switch (code) {
case ToxmeData::Updated:
GUI::showInfo(tr("Done!"), tr("Account %1@%2 updated successfully").arg(name, server));
Settings::getInstance().setToxme(name, server, bio, privacy);
showExistingToxme();
break;
case ToxmeData::Ok:
GUI::showInfo(tr("Done!"),
tr("Successfully added %1@%2 to the database. Save your password")
.arg(name, server));
Settings::getInstance().setToxme(name, server, bio, privacy, response);
showExistingToxme();
break;
default:
QString errorMessage = Toxme::getErrorMessage(code);
qWarning() << errorMessage;
QString translated = Toxme::translateErrorMessage(code);
GUI::showWarning(tr("Toxme error"), translated);
}
bodyUI->toxmeRegisterButton->setEnabled(true);
bodyUI->toxmeUpdateButton->setEnabled(true);
bodyUI->toxmeRegisterButton->setText(tr("Register"));
bodyUI->toxmeUpdateButton->setText(tr("Update"));
}
}