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
Anthony Bilinski 3a5e28fffb
fix(model): take QObject receiver argument to interface signal connection macro
receiver QObject is used by Qt to automatically deregister the connection when
the receiver is destroyed. Forward it on to Qt's connect.

(cherry picked from commit 24e4ec3751)
2020-03-20 02:07:20 -07:00

471 lines
16 KiB
C++

/*
Copyright © 2014-2019 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/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();
bodyUI->qrLabel->setWordWrap(true);
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);
profileInfo->connectTo_idChanged(this, [=](const ToxId& id) { setToxId(id); });
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);
profileInfo->connectTo_usernameChanged(
this,
[=](const QString& val) { bodyUI->userName->setText(val); });
profileInfo->connectTo_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)."));
}