/* * nheko Copyright (C) 2017 Konstantinos Sideris * * 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 #include #include #include #include #include #include #include #include #include #include "Config.h" #include "MatrixClient.h" #include "Olm.h" #include "UserSettingsPage.h" #include "Utils.h" #include "ui/FlatButton.h" #include "ui/ToggleButton.h" #include "version.h" UserSettings::UserSettings() { load(); } void UserSettings::load() { QSettings settings; isTrayEnabled_ = settings.value("user/window/tray", false).toBool(); hasDesktopNotifications_ = settings.value("user/desktop_notifications", true).toBool(); isStartInTrayEnabled_ = settings.value("user/window/start_in_tray", false).toBool(); isOrderingEnabled_ = settings.value("user/room_ordering", true).toBool(); isGroupViewEnabled_ = settings.value("user/group_view", true).toBool(); isTypingNotificationsEnabled_ = settings.value("user/typing_notifications", true).toBool(); isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", "light").toString(); applyTheme(); } void UserSettings::setTheme(QString theme) { theme_ = theme; save(); applyTheme(); } void UserSettings::applyTheme() { QFile stylefile; if (theme() == "light") { stylefile.setFileName(":/styles/styles/nheko.qss"); } else if (theme() == "dark") { stylefile.setFileName(":/styles/styles/nheko-dark.qss"); } else { stylefile.setFileName(":/styles/styles/system.qss"); } stylefile.open(QFile::ReadOnly); QString stylesheet = QString(stylefile.readAll()); qobject_cast(QApplication::instance())->setStyleSheet(stylesheet); } void UserSettings::save() { QSettings settings; settings.beginGroup("user"); settings.beginGroup("window"); settings.setValue("tray", isTrayEnabled_); settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); settings.setValue("room_ordering", isOrderingEnabled_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); settings.setValue("read_receipts", isReadReceiptsEnabled_); settings.setValue("group_view", isGroupViewEnabled_); settings.setValue("desktop_notifications", hasDesktopNotifications_); settings.setValue("theme", theme()); settings.endGroup(); } HorizontalLine::HorizontalLine(QWidget *parent) : QFrame{parent} { setFrameShape(QFrame::HLine); setFrameShadow(QFrame::Sunken); } UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidget *parent) : QWidget{parent} , settings_{settings} { topLayout_ = new QVBoxLayout(this); QIcon icon; icon.addFile(":/icons/icons/ui/angle-pointing-to-left.png"); auto backBtn_ = new FlatButton(this); backBtn_->setMinimumSize(QSize(24, 24)); backBtn_->setIcon(icon); backBtn_->setIconSize(QSize(24, 24)); QFont font; font.setPointSizeF(font.pointSizeF() * 1.1); auto versionInfo = new QLabel(QString("%1 | %2").arg(nheko::version).arg(nheko::build_os)); versionInfo->setTextInteractionFlags(Qt::TextBrowserInteraction); topBarLayout_ = new QHBoxLayout; topBarLayout_->setSpacing(0); topBarLayout_->setMargin(0); topBarLayout_->addWidget(backBtn_, 1, Qt::AlignLeft | Qt::AlignVCenter); topBarLayout_->addStretch(1); auto trayOptionLayout_ = new QHBoxLayout; trayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto trayLabel = new QLabel(tr("Minimize to tray"), this); trayLabel->setFont(font); trayToggle_ = new Toggle(this); trayOptionLayout_->addWidget(trayLabel); trayOptionLayout_->addWidget(trayToggle_, 0, Qt::AlignRight); auto startInTrayOptionLayout_ = new QHBoxLayout; startInTrayOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto startInTrayLabel = new QLabel(tr("Start in tray"), this); startInTrayLabel->setFont(font); startInTrayToggle_ = new Toggle(this); if (!settings_->isTrayEnabled()) startInTrayToggle_->setDisabled(true); startInTrayOptionLayout_->addWidget(startInTrayLabel); startInTrayOptionLayout_->addWidget(startInTrayToggle_, 0, Qt::AlignRight); auto orderRoomLayout = new QHBoxLayout; orderRoomLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto orderLabel = new QLabel(tr("Re-order rooms based on activity"), this); orderLabel->setFont(font); roomOrderToggle_ = new Toggle(this); orderRoomLayout->addWidget(orderLabel); orderRoomLayout->addWidget(roomOrderToggle_, 0, Qt::AlignRight); auto groupViewLayout = new QHBoxLayout; groupViewLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto groupViewLabel = new QLabel(tr("Group's sidebar"), this); groupViewLabel->setFont(font); groupViewToggle_ = new Toggle(this); groupViewLayout->addWidget(groupViewLabel); groupViewLayout->addWidget(groupViewToggle_, 0, Qt::AlignRight); auto typingLayout = new QHBoxLayout; typingLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto typingLabel = new QLabel(tr("Typing notifications"), this); typingLabel->setFont(font); typingNotifications_ = new Toggle(this); typingLayout->addWidget(typingLabel); typingLayout->addWidget(typingNotifications_, 0, Qt::AlignRight); auto receiptsLayout = new QHBoxLayout; receiptsLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto receiptsLabel = new QLabel(tr("Read receipts"), this); receiptsLabel->setFont(font); readReceipts_ = new Toggle(this); receiptsLayout->addWidget(receiptsLabel); receiptsLayout->addWidget(readReceipts_, 0, Qt::AlignRight); auto desktopLayout = new QHBoxLayout; desktopLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto desktopLabel = new QLabel(tr("Desktop notifications"), this); desktopLabel->setFont(font); desktopNotifications_ = new Toggle(this); desktopLayout->addWidget(desktopLabel); desktopLayout->addWidget(desktopNotifications_, 0, Qt::AlignRight); auto scaleFactorOptionLayout = new QHBoxLayout; scaleFactorOptionLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto scaleFactorLabel = new QLabel(tr("Scale factor (requires restart)"), this); scaleFactorLabel->setFont(font); scaleFactorCombo_ = new QComboBox(this); scaleFactorCombo_->addItem("1"); scaleFactorCombo_->addItem("1.25"); scaleFactorCombo_->addItem("1.5"); scaleFactorCombo_->addItem("1.75"); scaleFactorCombo_->addItem("2"); scaleFactorCombo_->addItem("2.25"); scaleFactorCombo_->addItem("2.5"); scaleFactorCombo_->addItem("2.75"); scaleFactorCombo_->addItem("3"); scaleFactorOptionLayout->addWidget(scaleFactorLabel); scaleFactorOptionLayout->addWidget(scaleFactorCombo_, 0, Qt::AlignRight); auto themeOptionLayout_ = new QHBoxLayout; themeOptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto themeLabel_ = new QLabel(tr("Theme"), this); themeLabel_->setFont(font); themeCombo_ = new QComboBox(this); themeCombo_->addItem("Light"); themeCombo_->addItem("Dark"); themeCombo_->addItem("System"); themeOptionLayout_->addWidget(themeLabel_); themeOptionLayout_->addWidget(themeCombo_, 0, Qt::AlignRight); auto encryptionLayout_ = new QVBoxLayout; encryptionLayout_->setContentsMargins(0, OptionMargin, 0, OptionMargin); encryptionLayout_->setAlignment(Qt::AlignVCenter); QFont monospaceFont; monospaceFont.setFamily("Monospace"); monospaceFont.setStyleHint(QFont::Monospace); monospaceFont.setPointSizeF(monospaceFont.pointSizeF() * 0.9); auto deviceIdLayout = new QHBoxLayout; deviceIdLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto deviceIdLabel = new QLabel(tr("Device ID"), this); deviceIdLabel->setFont(font); deviceIdLabel->setMargin(0); deviceIdValue_ = new QLabel{this}; deviceIdValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceIdValue_->setFont(monospaceFont); deviceIdLayout->addWidget(deviceIdLabel, 1); deviceIdLayout->addWidget(deviceIdValue_); auto deviceFingerprintLayout = new QHBoxLayout; deviceFingerprintLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto deviceFingerprintLabel = new QLabel(tr("Device Fingerprint"), this); deviceFingerprintLabel->setFont(font); deviceFingerprintLabel->setMargin(0); deviceFingerprintValue_ = new QLabel{this}; deviceFingerprintValue_->setTextInteractionFlags(Qt::TextSelectableByMouse); deviceFingerprintValue_->setFont(monospaceFont); deviceFingerprintLayout->addWidget(deviceFingerprintLabel, 1); deviceFingerprintLayout->addWidget(deviceFingerprintValue_); auto sessionKeysLayout = new QHBoxLayout; sessionKeysLayout->setContentsMargins(0, OptionMargin, 0, OptionMargin); auto sessionKeysLabel = new QLabel(tr("Session Keys"), this); sessionKeysLabel->setFont(font); sessionKeysLayout->addWidget(sessionKeysLabel, 1); auto sessionKeysImportBtn = new QPushButton{tr("IMPORT"), this}; connect( sessionKeysImportBtn, &QPushButton::clicked, this, &UserSettingsPage::importSessionKeys); auto sessionKeysExportBtn = new QPushButton{tr("EXPORT"), this}; connect( sessionKeysExportBtn, &QPushButton::clicked, this, &UserSettingsPage::exportSessionKeys); sessionKeysLayout->addWidget(sessionKeysExportBtn, 0, Qt::AlignRight); sessionKeysLayout->addWidget(sessionKeysImportBtn, 0, Qt::AlignRight); encryptionLayout_->addLayout(deviceIdLayout); encryptionLayout_->addLayout(deviceFingerprintLayout); encryptionLayout_->addWidget(new HorizontalLine{this}); encryptionLayout_->addLayout(sessionKeysLayout); font.setWeight(65); auto encryptionLabel_ = new QLabel(tr("ENCRYPTION"), this); encryptionLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); encryptionLabel_->setFont(font); auto general_ = new QLabel(tr("GENERAL"), this); general_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); general_->setFont(font); mainLayout_ = new QVBoxLayout; mainLayout_->setAlignment(Qt::AlignTop); mainLayout_->setSpacing(7); mainLayout_->setContentsMargins( sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); mainLayout_->addWidget(general_, 1, Qt::AlignLeft | Qt::AlignBottom); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(trayOptionLayout_); mainLayout_->addLayout(startInTrayOptionLayout_); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(orderRoomLayout); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(groupViewLayout); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(typingLayout); mainLayout_->addLayout(receiptsLayout); mainLayout_->addLayout(desktopLayout); mainLayout_->addWidget(new HorizontalLine(this)); #if defined(Q_OS_MAC) scaleFactorLabel->hide(); scaleFactorCombo_->hide(); #else mainLayout_->addLayout(scaleFactorOptionLayout); mainLayout_->addWidget(new HorizontalLine(this)); #endif mainLayout_->addLayout(themeOptionLayout_); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addSpacing(50); mainLayout_->addWidget(encryptionLabel_, 1, Qt::AlignLeft | Qt::AlignBottom); mainLayout_->addWidget(new HorizontalLine(this)); mainLayout_->addLayout(encryptionLayout_); auto scrollArea_ = new QScrollArea(this); scrollArea_->setFrameShape(QFrame::NoFrame); scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); scrollArea_->setWidgetResizable(true); scrollArea_->setAlignment(Qt::AlignTop | Qt::AlignVCenter); auto scrollAreaContents_ = new QWidget(this); scrollAreaContents_->setObjectName("UserSettingScrollWidget"); scrollAreaContents_->setLayout(mainLayout_); scrollArea_->setWidget(scrollAreaContents_); topLayout_->addLayout(topBarLayout_); topLayout_->addWidget(scrollArea_, Qt::AlignTop); topLayout_->addStretch(1); topLayout_->addWidget(versionInfo); connect(themeCombo_, static_cast(&QComboBox::activated), [this](const QString &text) { settings_->setTheme(text.toLower()); }); connect(scaleFactorCombo_, static_cast(&QComboBox::activated), [](const QString &factor) { utils::setScaleFactor(factor.toFloat()); }); connect(trayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTray(!isDisabled); if (isDisabled) { startInTrayToggle_->setDisabled(true); } else { startInTrayToggle_->setEnabled(true); } emit trayOptionChanged(!isDisabled); }); connect(startInTrayToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setStartInTray(!isDisabled); }); connect(roomOrderToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setRoomOrdering(!isDisabled); }); connect(groupViewToggle_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setGroupView(!isDisabled); }); connect(typingNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setTypingNotifications(!isDisabled); }); connect(readReceipts_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setReadReceipts(!isDisabled); }); connect(desktopNotifications_, &Toggle::toggled, this, [this](bool isDisabled) { settings_->setDesktopNotifications(!isDisabled); }); connect(backBtn_, &QPushButton::clicked, this, [this]() { settings_->save(); emit moveBack(); }); } void UserSettingsPage::showEvent(QShowEvent *) { restoreThemeCombo(); restoreScaleFactor(); // FIXME: Toggle treats true as "off" trayToggle_->setState(!settings_->isTrayEnabled()); startInTrayToggle_->setState(!settings_->isStartInTrayEnabled()); roomOrderToggle_->setState(!settings_->isOrderingEnabled()); groupViewToggle_->setState(!settings_->isGroupViewEnabled()); typingNotifications_->setState(!settings_->isTypingNotificationsEnabled()); readReceipts_->setState(!settings_->isReadReceiptsEnabled()); desktopNotifications_->setState(!settings_->hasDesktopNotifications()); deviceIdValue_->setText(QString::fromStdString(http::client()->device_id())); deviceFingerprintValue_->setText( utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519)); } void UserSettingsPage::resizeEvent(QResizeEvent *event) { sideMargin_ = width() * 0.2; mainLayout_->setContentsMargins( sideMargin_, LayoutTopMargin, sideMargin_, LayoutBottomMargin); QWidget::resizeEvent(event); } void UserSettingsPage::paintEvent(QPaintEvent *) { QStyleOption opt; opt.init(this); QPainter p(this); style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); } void UserSettingsPage::restoreScaleFactor() const { auto factor = utils::scaleFactor(); if (factor == 1) scaleFactorCombo_->setCurrentIndex(0); else if (factor == 1.25) scaleFactorCombo_->setCurrentIndex(1); else if (factor == 1.5) scaleFactorCombo_->setCurrentIndex(2); else if (factor == 1.75) scaleFactorCombo_->setCurrentIndex(3); else if (factor == 2) scaleFactorCombo_->setCurrentIndex(4); else if (factor == 2.25) scaleFactorCombo_->setCurrentIndex(5); else if (factor == 2.5) scaleFactorCombo_->setCurrentIndex(6); else if (factor == 2.75) scaleFactorCombo_->setCurrentIndex(7); else if (factor == 3) scaleFactorCombo_->setCurrentIndex(7); else scaleFactorCombo_->setCurrentIndex(0); } void UserSettingsPage::restoreThemeCombo() const { if (settings_->theme() == "light") themeCombo_->setCurrentIndex(0); else if (settings_->theme() == "dark") themeCombo_->setCurrentIndex(1); else themeCombo_->setCurrentIndex(2); } void UserSettingsPage::importSessionKeys() { auto fileName = QFileDialog::getOpenFileName(this, tr("Open Sessions File"), "", ""); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr("Error"), file.errorString()); return; } auto bin = file.peek(file.size()); auto payload = std::string(bin.data(), bin.size()); bool ok; auto password = QInputDialog::getText(this, tr("File Password"), tr("Enter the passphrase to decrypt the file:"), QLineEdit::Password, "", &ok); if (!ok) return; if (password.isEmpty()) { QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); return; } try { auto sessions = mtx::crypto::decrypt_exported_sessions( mtx::crypto::base642bin(payload), password.toStdString()); cache::client()->importSessionKeys(std::move(sessions)); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const nlohmann::json::exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } } void UserSettingsPage::exportSessionKeys() { // Open password dialog. bool ok; auto password = QInputDialog::getText(this, tr("File Password"), tr("Enter passphrase to encrypt your session keys:"), QLineEdit::Password, "", &ok); if (!ok) return; if (password.isEmpty()) { QMessageBox::warning(this, tr("Error"), tr("The password cannot be empty")); return; } // Open file dialog to save the file. auto fileName = QFileDialog::getSaveFileName(this, tr("File to save the exported session keys"), "", ""); QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, tr("Error"), file.errorString()); return; } // Export sessions & save to file. try { auto encrypted_blob = mtx::crypto::encrypt_exported_sessions( cache::client()->exportSessionKeys(), password.toStdString()); auto b64 = mtx::crypto::bin2base64(encrypted_blob); file.write(b64.data(), b64.size()); } catch (const mtx::crypto::sodium_exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const lmdb::error &e) { QMessageBox::warning(this, tr("Error"), e.what()); } catch (const nlohmann::json::exception &e) { QMessageBox::warning(this, tr("Error"), e.what()); } }