diff --git a/CMakeLists.txt b/CMakeLists.txt index 970c14a4..9e60648a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,8 +67,13 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Declarative Package ) -find_package(KF6KirigamiAddons 0.6 REQUIRED) +find_package(PkgConfig REQUIRED) + +pkg_check_modules(GOBJECT gobject-2.0 REQUIRED IMPORTED_TARGET) +pkg_check_modules(GIO gio-2.0 REQUIRED IMPORTED_TARGET) + +find_package(KF6KirigamiAddons 0.6 REQUIRED) find_package(LibKWorkspace CONFIG REQUIRED) find_package(KWinDBusInterface) diff --git a/kcms/CMakeLists.txt b/kcms/CMakeLists.txt index 82bc4723..5df810d9 100644 --- a/kcms/CMakeLists.txt +++ b/kcms/CMakeLists.txt @@ -2,3 +2,8 @@ # SPDX-License-Identifier: GPL-2.0-or-later add_subdirectory(mobileshell) +add_subdirectory(cellularnetwork) +add_subdirectory(info) +add_subdirectory(powermanagement) +add_subdirectory(time) +add_subdirectory(virtualkeyboard) diff --git a/kcms/cellularnetwork/CMakeLists.txt b/kcms/cellularnetwork/CMakeLists.txt new file mode 100644 index 00000000..92bd77d2 --- /dev/null +++ b/kcms/cellularnetwork/CMakeLists.txt @@ -0,0 +1,26 @@ +set (cellularnetworksettings_SRCS + cellularnetworksettings.cpp + modem.cpp + modemdetails.cpp + sim.cpp + mobileproviders.cpp + profilesettings.cpp +) + +add_library(kcm_cellular_network MODULE ${cellularnetworksettings_SRCS}) + +target_link_libraries(kcm_cellular_network + Qt::DBus + Qt::Gui + Qt::Quick + Qt::Qml + Qt::Xml + KF6::Plasma + KF6::I18n + KF6::NetworkManagerQt + KF6::ModemManagerQt + KF6::QuickAddons +) + +install(TARGETS kcm_cellular_network DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) +kpackage_install_package(package kcm_cellular_network kcms) diff --git a/kcms/cellularnetwork/Messages.sh b/kcms/cellularnetwork/Messages.sh new file mode 100644 index 00000000..f6cafef0 --- /dev/null +++ b/kcms/cellularnetwork/Messages.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_cellular_network.pot diff --git a/kcms/cellularnetwork/cellularnetworksettings.cpp b/kcms/cellularnetwork/cellularnetworksettings.cpp new file mode 100644 index 00000000..01f43789 --- /dev/null +++ b/kcms/cellularnetwork/cellularnetworksettings.cpp @@ -0,0 +1,157 @@ +/* + SPDX-FileCopyrightText: 2018 Martin Kacej + SPDX-FileCopyrightText: 2020-2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#include "cellularnetworksettings.h" + +#include +#include +#include + +#include + +K_PLUGIN_CLASS_WITH_JSON(CellularNetworkSettings, "cellularnetworksettings.json") + +CellularNetworkSettings *CellularNetworkSettings::staticInst = nullptr; + +CellularNetworkSettings::CellularNetworkSettings(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, metaData, args) + , m_modemList{} + , m_simList{} +{ + CellularNetworkSettings::staticInst = this; + + qmlRegisterType("cellularnetworkkcm", 1, 0, "ProfileSettings"); + qmlRegisterType("cellularnetworkkcm", 1, 0, "Modem"); + qmlRegisterType("cellularnetworkkcm", 1, 0, "ModemDetails"); + qmlRegisterType("cellularnetworkkcm", 1, 0, "AvailableNetwork"); + qmlRegisterType("cellularnetworkkcm", 1, 0, "Sim"); + qmlRegisterType("cellularnetworkkcm", 1, 0, "InlineMessage"); + + // find modems + updateModemList(); + + connect(ModemManager::notifier(), &ModemManager::Notifier::modemAdded, this, &CellularNetworkSettings::updateModemList); + connect(ModemManager::notifier(), &ModemManager::Notifier::modemRemoved, this, &CellularNetworkSettings::updateModemList); +} + +CellularNetworkSettings *CellularNetworkSettings::instance() +{ + return CellularNetworkSettings::staticInst; +} + +Modem *CellularNetworkSettings::selectedModem() +{ + // TODO: we are currently assuming there is a single modem + if (m_modemList.count() > 0) { + return m_modemList[0]; + } + return nullptr; +} + +QList CellularNetworkSettings::modems() +{ + return m_modemList; +} + +QList CellularNetworkSettings::sims() +{ + return m_simList; +} + +bool CellularNetworkSettings::modemFound() +{ + return !m_modemList.empty(); +} + +void CellularNetworkSettings::updateModemList() +{ + // find modems + ModemManager::scanDevices(); + + qDebug() << QStringLiteral("Scanning for modems..."); + + // loop over every modem + for (ModemManager::ModemDevice::Ptr device : ModemManager::modemDevices()) { + ModemManager::Modem::Ptr modem = device->modemInterface(); + + qDebug() << QStringLiteral("Found modem:") << device->uni(); + + m_modemList.push_back(new Modem(this, device, modem)); + + // update sims list if modem's list changes + connect(m_modemList[m_modemList.size() - 1], &Modem::simsChanged, this, [this]() -> void { + fillSims(); + }); + } + + if (m_modemList.empty()) { + qDebug() << QStringLiteral("No modems found."); + } + + // fill sim list + fillSims(); + + // update the currently selected modem + Q_EMIT selectedModemChanged(); +} + +void CellularNetworkSettings::fillSims() +{ + for (auto p : m_simList) { + delete p; + } + m_simList.clear(); + + qDebug() << QStringLiteral("Scanning SIMs list..."); + for (auto modem : m_modemList) { + auto sims = modem->sims(); + for (auto sim : sims) { + qDebug() << QStringLiteral("Found SIM") << sim->uni() << sim->imsi(); + m_simList.push_back(sim); + } + } + + Q_EMIT simsChanged(); +} + +QList CellularNetworkSettings::messages() +{ + return m_messages; +} + +void CellularNetworkSettings::addMessage(InlineMessage::Type type, QString msg) +{ + m_messages.push_back(new InlineMessage{this, type, msg}); + Q_EMIT messagesChanged(); +} + +void CellularNetworkSettings::removeMessage(int index) +{ + if (index >= 0 && index < m_messages.size()) { + m_messages.removeAt(index); + Q_EMIT messagesChanged(); + } +} + +InlineMessage::InlineMessage(QObject *parent, Type type, QString message) + : QObject{parent} + , m_type{type} + , m_message{message} +{ +} + +InlineMessage::Type InlineMessage::type() +{ + return m_type; +} + +QString InlineMessage::message() +{ + return m_message; +} + +#include "cellularnetworksettings.moc" diff --git a/kcms/cellularnetwork/cellularnetworksettings.h b/kcms/cellularnetwork/cellularnetworksettings.h new file mode 100644 index 00000000..d746a97c --- /dev/null +++ b/kcms/cellularnetwork/cellularnetworksettings.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2018 Martin Kacej + SPDX-FileCopyrightText: 2020-2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#pragma once + +#include + +#include + +#include "mobileproviders.h" +#include "modem.h" +#include "modemdetails.h" +#include "sim.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class Sim; +class Modem; +class MobileProviders; + +class InlineMessage : public QObject +{ + Q_OBJECT + Q_PROPERTY(int type READ type NOTIFY typeChanged) + Q_PROPERTY(QString message READ message NOTIFY messageChanged) + +public: + enum Type { + Information, + Positive, + Warning, + Error, + }; + + InlineMessage(QObject *parent = nullptr, Type type = Information, QString message = ""); + + Type type(); + QString message(); + +Q_SIGNALS: + void typeChanged(); + void messageChanged(); + +private: + Type m_type; + QString m_message; +}; + +class CellularNetworkSettings : public KQuickAddons::ConfigModule +{ + Q_OBJECT + Q_PROPERTY(bool modemFound READ modemFound NOTIFY modemFoundChanged) + Q_PROPERTY(Modem *selectedModem READ selectedModem NOTIFY selectedModemChanged) + Q_PROPERTY(QList sims READ sims NOTIFY simsChanged) + Q_PROPERTY(QList messages READ messages NOTIFY messagesChanged) + +public: + CellularNetworkSettings(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args); + + static CellularNetworkSettings *instance(); + + Modem *selectedModem(); + QList modems(); + QList sims(); + + bool modemFound(); + + QList messages(); + void addMessage(InlineMessage::Type type, QString msg); + Q_INVOKABLE void removeMessage(int index); + +Q_SIGNALS: + void modemFoundChanged(); + void selectedModemChanged(); + void simsChanged(); + void messagesChanged(); + +private: + void updateModemList(); + void fillSims(); + + QList m_modemList; + QList m_simList; + + QList m_messages; + + static CellularNetworkSettings *staticInst; +}; diff --git a/kcms/cellularnetwork/cellularnetworksettings.json b/kcms/cellularnetwork/cellularnetworksettings.json new file mode 100644 index 00000000..266f4d07 --- /dev/null +++ b/kcms/cellularnetwork/cellularnetworksettings.json @@ -0,0 +1,114 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-system;", + "KPlugin": { + "Description": "Management of cellular networks", + "Description[az]": "Mobil şəbəkələr idarəetməsi", + "Description[ca@valencia]": "Gestió de xarxes de telefonia mòbil", + "Description[ca]": "Gestió de xarxes de telefonia mòbil", + "Description[cs]": "Spravovat vaše mobilní sítě", + "Description[de]": "Verwaltung von Mobilfunknetzen", + "Description[en_GB]": "Management of cellular networks", + "Description[es]": "Gestión de redes móviles", + "Description[eu]": "Mugikor-sareen kudeaketa", + "Description[fi]": "Matkapuhelinverkkojen hallinta", + "Description[fr]": "Gestion de réseaux cellulaires", + "Description[hu]": "Mobilhálózatok kezelése", + "Description[ia]": "Gestion de retes cellular", + "Description[id]": "Pengelolaan jaringan seluler", + "Description[is]": "Umsjón farsímaneta", + "Description[it]": "Gestione delle reti cellulari", + "Description[ka]": "ფიჭური ქსელების მართვა", + "Description[ko]": "모바일 네트워크 관리", + "Description[lt]": "Korinių tinklų tvarkymas", + "Description[nl]": "Beheer van cellulaire netwerken", + "Description[nn]": "Handsaming av mobilnettverk", + "Description[pa]": "ਸੈਲੂਲਰ ਨੈੱਟਵਰਕਾਂ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ", + "Description[pl]": "Zarządzanie sieciami komórkowymi", + "Description[pt]": "Gestão de redes móveis", + "Description[pt_BR]": "Gerenciamento de redes celulares", + "Description[ro]": "Gestiunea rețelelor celulare", + "Description[ru]": "Управление сотовыми сетями", + "Description[sl]": "Upravljanje mobilnih omrežij", + "Description[sv]": "Hantering av mobilnät", + "Description[tr]": "Hücresel ağların yönetimi", + "Description[uk]": "Керування стільниковими мережами", + "Description[vi]": "Quản lí các mạng di dộng", + "Description[x-test]": "xxManagement of cellular networksxx", + "Description[zh_CN]": "管理移动网络", + "Description[zh_TW]": "行動網路管理", + "FormFactors": [ + "handset", + "tablet", + "mediacenter" + ], + "Icon": "smartphone", + "Name": "Cellular Network", + "Name[az]": "Mobil Şəbəkə", + "Name[ca@valencia]": "Xarxa de telefonia mòbil", + "Name[ca]": "Xarxa de telefonia mòbil", + "Name[cs]": "Mobilní síť", + "Name[de]": "Mobilfunknetz", + "Name[en_GB]": "Cellular Network", + "Name[es]": "Red móvil", + "Name[eu]": "Mugikorrerako sareak", + "Name[fi]": "Matkapuhelinverkko", + "Name[fr]": "Réseau cellulaire", + "Name[hu]": "Mobilhálózatok", + "Name[ia]": "Rete Cellular", + "Name[id]": "Jaringan Seluler", + "Name[is]": "Farsímakerfi", + "Name[it]": "Rete cellulare", + "Name[ka]": "ფიჭური ქსელი", + "Name[ko]": "모바일 네트워크", + "Name[lt]": "Korinis tinklas", + "Name[nl]": "Cellulair netwerk", + "Name[nn]": "Mobilnettverk", + "Name[pa]": "ਸੈਲੂਲਰ ਨੈੱਟਵਰਕ", + "Name[pl]": "Sieci komórkowe", + "Name[pt]": "Rede Móvel", + "Name[pt_BR]": "Rede celular", + "Name[ro]": "Rețea celulară", + "Name[ru]": "Сотовая сеть", + "Name[sk]": "Mobilná sieť", + "Name[sl]": "Mobilno omrežje", + "Name[sv]": "Mobilnät", + "Name[tr]": "Hücresel Ağ", + "Name[uk]": "Стільникова мережа", + "Name[vi]": "Mạng di động", + "Name[x-test]": "xxCellular Networkxx", + "Name[zh_CN]": "移动网络", + "Name[zh_TW]": "行動網路" + }, + "X-KDE-Keywords": "mobile,data,network", + "X-KDE-Keywords[az]": "mobile,data,network,verilənlər,şəbəkə,mobil", + "X-KDE-Keywords[ca@valencia]": "mòbil,dades,xarxa", + "X-KDE-Keywords[ca]": "mòbil,dades,xarxa", + "X-KDE-Keywords[cs]": "mobilní,data,síť", + "X-KDE-Keywords[de]": "mobil,daten,netzwerk,Mobiltelefon", + "X-KDE-Keywords[en_GB]": "mobile,data,network", + "X-KDE-Keywords[es]": "móvil,datos,red", + "X-KDE-Keywords[eu]": "mugikorra,datuak,sarea", + "X-KDE-Keywords[fi]": "mobiili,data,verkko", + "X-KDE-Keywords[fr]": "mobile, donnée, réseau", + "X-KDE-Keywords[hu]": "mobil,adat,hálózat", + "X-KDE-Keywords[ia]": "mobile,data,network", + "X-KDE-Keywords[it]": "mobile,dati,rete", + "X-KDE-Keywords[ko]": "mobile,data,network,모바일,데이터,네트워크", + "X-KDE-Keywords[lt]": "mobilusis,mobilieji,duomenys,tinklas", + "X-KDE-Keywords[nl]": "mobiel,gegevens,netwerk", + "X-KDE-Keywords[nn]": "mobil,mobilt,data,nettverk", + "X-KDE-Keywords[pa]": "ਮੋਬਾਈਲ,ਡਾਟਾ,ਨੈੱਟਵਰਕ", + "X-KDE-Keywords[pl]": "mobilne,komórkowe,przenośne,dane,sieć", + "X-KDE-Keywords[pt]": "móvel,dados,rede", + "X-KDE-Keywords[pt_BR]": "móvel,dados,rede", + "X-KDE-Keywords[ru]": "mobile,data,network,мобильная сеть,данные,сеть", + "X-KDE-Keywords[sk]": "mobilné,dáta,sieť", + "X-KDE-Keywords[sl]": "mobilno,podatki,omrežje", + "X-KDE-Keywords[sv]": "mobil,data,nätverk", + "X-KDE-Keywords[uk]": "mobile,data,network,мобільний,дані,мережа", + "X-KDE-Keywords[vi]": "mobile,data,network,di động,dữ liệu,mạng", + "X-KDE-Keywords[x-test]": "xxmobilexx,xxdataxx,xxnetworkxx", + "X-KDE-Keywords[zh_CN]": "mobile,data,network,移动,数据,网络,蜂窝,手机", + "X-KDE-System-Settings-Parent-Category": "network", + "X-KDE-Weight": 70 +} diff --git a/kcms/cellularnetwork/mobileproviders.cpp b/kcms/cellularnetwork/mobileproviders.cpp new file mode 100644 index 00000000..82e4332b --- /dev/null +++ b/kcms/cellularnetwork/mobileproviders.cpp @@ -0,0 +1,336 @@ +/* + SPDX-FileCopyrightText: 2010-2012 Lamarque Souza + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "mobileproviders.h" + +#include +#include +#include +#include +#include + +const QString MobileProviders::ProvidersFile = QStringLiteral("/usr/share/mobile-broadband-provider-info/serviceproviders.xml"); + +// adapted from https://invent.kde.org/plasma/plasma-nm/-/blob/master/libs/editor/mobileproviders.cpp +// we only use gsm, ignore cdma + +bool localeAwareCompare(const QString &one, const QString &two) +{ + return one.localeAwareCompare(two) < 0; +} + +MobileProviders::MobileProviders() +{ + for (int c = 1; c <= QLocale::LastCountry; c++) { + const auto country = static_cast(c); + QLocale locale(QLocale::AnyLanguage, country); + if (locale.country() == country) { + const QString localeName = locale.name(); + const auto idx = localeName.indexOf(QLatin1Char('_')); + if (idx != -1) { + const QString countryCode = localeName.mid(idx + 1); + QString countryName = locale.nativeCountryName(); + if (countryName.isEmpty()) { + countryName = QLocale::countryToString(country); + } + mCountries.insert(countryCode, countryName); + } + } + } + mError = Success; + + QFile file2(ProvidersFile); + + if (file2.open(QIODevice::ReadOnly)) { + if (mDocProviders.setContent(&file2)) { + docElement = mDocProviders.documentElement(); + + if (docElement.isNull()) { + qWarning() << ProvidersFile << ": document is null"; + mError = ProvidersIsNull; + } else { + if (docElement.isNull() || docElement.tagName() != "serviceproviders") { + qWarning() << ProvidersFile << ": wrong format"; + mError = ProvidersWrongFormat; + } else { + if (docElement.attribute("format") != "2.0") { + qWarning() << ProvidersFile << ": mobile broadband provider database format '" << docElement.attribute("format") << "' not supported."; + mError = ProvidersFormatNotSupported; + } else { + // qCDebug(PLASMA_NM) << "Everything is alright so far"; + } + } + } + } + + file2.close(); + } else { + qWarning() << "Error opening providers file" << ProvidersFile; + mError = ProvidersMissing; + } +} + +MobileProviders::~MobileProviders() +{ +} + +QStringList MobileProviders::getCountryList() const +{ + QStringList temp = mCountries.values(); + std::sort(temp.begin(), temp.end(), localeAwareCompare); + return temp; +} + +QString MobileProviders::countryFromLocale() const +{ + const QString localeName = QLocale().name(); + const auto idx = localeName.indexOf(QLatin1Char('_')); + if (idx != -1) { + return localeName.mid(idx + 1); + } + return QString(); +} + +QStringList MobileProviders::getApns(const QString &provider) +{ + mApns.clear(); + mNetworkIds.clear(); + if (!mProvidersGsm.contains(provider)) { + return QStringList(); + } + + QDomNode n = mProvidersGsm[provider]; + + while (!n.isNull()) { + QDomElement e = n.toElement(); // + + if (!e.isNull() && e.tagName().toLower() == "gsm") { + QDomNode n2 = e.firstChild(); + while (!n2.isNull()) { + QDomElement e2 = n2.toElement(); // + + if (!e2.isNull() && e2.tagName().toLower() == "apn") { + bool isInternet = true; + QDomNode n3 = e2.firstChild(); + while (!n3.isNull()) { + QDomElement e3 = n3.toElement(); // + if (!e3.isNull() && e3.tagName().toLower() == "usage" && !e3.attribute("type").isNull() + && e3.attribute("type").toLower() != "internet") { + // qCDebug(PLASMA_NM) << "apn" << e2.attribute("value") << "ignored because of usage" << e3.attribute("type"); + isInternet = false; + break; + } + n3 = n3.nextSibling(); + } + if (isInternet) { + mApns.insert(e2.attribute("value"), e2.firstChild()); + } + } else if (!e2.isNull() && e2.tagName().toLower() == "network-id") { + mNetworkIds.append(e2.attribute("mcc") + '-' + e2.attribute("mnc")); + } + + n2 = n2.nextSibling(); + } + } + n = n.nextSibling(); + } + + QStringList temp = mApns.keys(); + temp.sort(); + return temp; +} + +ProviderData MobileProviders::parseProvider(const QDomNode &providerNode) +{ + ProviderData result; + + QMap localizedProviderNames; + + QDomNode c = providerNode.firstChild(); // + bool hasGsm = false; + + while (!c.isNull()) { + QDomElement ce = c.toElement(); + + if (ce.tagName().toLower() == QLatin1String("gsm")) { + QDomNode gsmNode = c.firstChild(); + + while (!gsmNode.isNull()) { + QDomElement gsmElement = gsmNode.toElement(); + + if (gsmElement.tagName().toLower() == QLatin1String("network-id")) { + result.mccmncs.append(gsmElement.attribute("mcc") + gsmElement.attribute("mnc")); + } + gsmNode = gsmNode.nextSibling(); + } + + hasGsm = true; + } else if (ce.tagName().toLower() == QLatin1String("name")) { + QString lang = ce.attribute("xml:lang"); + if (lang.isEmpty()) { + lang = "en"; // English is default + } else { + lang = lang.toLower(); + lang.remove(QRegularExpression(QStringLiteral("\\-.*$"))); // Remove everything after '-' in xml:lang attribute. + } + localizedProviderNames.insert(lang, ce.text()); + } + + c = c.nextSibling(); + } + + result.name = getNameByLocale(localizedProviderNames); + + const QString name = result.name; + if (hasGsm) { + mProvidersGsm.insert(name, providerNode.firstChild()); + } + + return result; +} + +QStringList MobileProviders::getProvidersFromMCCMNC(const QString &targetMccMnc) +{ + QStringList result; + + QDomNode n = docElement.firstChild(); + + while (!n.isNull()) { + QDomElement e = n.toElement(); // + + if (!e.isNull()) { + QDomNode n2 = e.firstChild(); + while (!n2.isNull()) { + QDomElement e2 = n2.toElement(); // + + if (!e2.isNull() && e2.tagName().toLower() == "provider") { + ProviderData data = parseProvider(e2); + + if (data.mccmncs.contains(targetMccMnc)) { + result << data.name; + } + } + n2 = n2.nextSibling(); + } + } + n = n.nextSibling(); + } + + return result; +} + +QStringList MobileProviders::getNetworkIds(const QString &provider) +{ + if (mNetworkIds.isEmpty()) { + getApns(provider); + } + return mNetworkIds; +} + +QVariantMap MobileProviders::getApnInfo(const QString &apn) +{ + QVariantMap temp; + QDomNode n = mApns[apn]; + QStringList dnsList; + QMap localizedPlanNames; + + while (!n.isNull()) { + QDomElement e = n.toElement(); // + + if (!e.isNull()) { + if (e.tagName().toLower() == "name") { + QString lang = e.attribute("xml:lang"); + if (lang.isEmpty()) { + lang = "en"; // English is default + } else { + lang = lang.toLower(); + lang.remove(QRegularExpression(QStringLiteral("\\-.*$"))); // Remove everything after '-' in xml:lang attribute. + } + localizedPlanNames.insert(lang, e.text()); + } else if (e.tagName().toLower() == "username") { + temp.insert("username", e.text()); + } else if (e.tagName().toLower() == "password") { + temp.insert("password", e.text()); + } else if (e.tagName().toLower() == "dns") { + dnsList.append(e.text()); + } else if (e.tagName().toLower() == "usage") { + temp.insert("usageType", e.attribute("type")); + } + } + + n = n.nextSibling(); + } + + QString name = getNameByLocale(localizedPlanNames); + if (!name.isEmpty()) { + temp.insert("name", QVariant::fromValue(name)); + } + temp.insert("number", getGsmNumber()); + temp.insert("apn", apn); + temp.insert("dnsList", dnsList); + + return temp; +} + +QVariantMap MobileProviders::getCdmaInfo(const QString &provider) +{ + if (!mProvidersCdma.contains(provider)) { + return QVariantMap(); + } + + QVariantMap temp; + QDomNode n = mProvidersCdma[provider]; + QStringList sidList; + + while (!n.isNull()) { + QDomElement e = n.toElement(); // + + if (!e.isNull() && e.tagName().toLower() == "cdma") { + QDomNode n2 = e.firstChild(); + while (!n2.isNull()) { + QDomElement e2 = n2.toElement(); // + + if (!e2.isNull()) { + if (e2.tagName().toLower() == "username") { + temp.insert("username", e2.text()); + } else if (e2.tagName().toLower() == "password") { + temp.insert("password", e2.text()); + } else if (e2.tagName().toLower() == "sid") { + sidList.append(e2.text()); + } + } + + n2 = n2.nextSibling(); + } + } + n = n.nextSibling(); + } + + temp.insert("number", getCdmaNumber()); + temp.insert("sidList", sidList); + return temp; +} + +QString MobileProviders::getNameByLocale(const QMap &localizedNames) const +{ + QString name; + const QStringList locales = QLocale().uiLanguages(); + for (const QString &locale : locales) { + QString language = locale.split(QLatin1Char('-')).at(0); + + if (localizedNames.contains(language)) { + return localizedNames[language]; + } + } + + name = localizedNames["en"]; + + // Use any language if no proper localized name were found. + if (name.isEmpty() && !localizedNames.isEmpty()) { + name = localizedNames.constBegin().value(); + } + return name; +} diff --git a/kcms/cellularnetwork/mobileproviders.h b/kcms/cellularnetwork/mobileproviders.h new file mode 100644 index 00000000..e9de2cc5 --- /dev/null +++ b/kcms/cellularnetwork/mobileproviders.h @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2010-2012 Lamarque Souza + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef PLASMA_NM_MOBILE_PROVIDERS_H +#define PLASMA_NM_MOBILE_PROVIDERS_H + +#include +#include +#include +#include + +#include + +// adapted from https://invent.kde.org/plasma/plasma-nm/-/blob/master/libs/editor/mobileproviders.h +// we only use gsm, ignore cdma + +struct ProviderData { + QStringList mccmncs; + QString name; +}; + +class Q_DECL_EXPORT MobileProviders +{ +public: + static const QString ProvidersFile; + + enum ErrorCodes { + Success, + CountryCodesMissing, + ProvidersMissing, + ProvidersIsNull, + ProvidersWrongFormat, + ProvidersFormatNotSupported, + }; + + MobileProviders(); + ~MobileProviders(); + + QStringList getCountryList() const; + QString countryFromLocale() const; + QString getCountryName(const QString &key) const + { + return mCountries.value(key); + } + QStringList getApns(const QString &provider); + QStringList getNetworkIds(const QString &provider); + QVariantMap getApnInfo(const QString &apn); + QVariantMap getCdmaInfo(const QString &provider); + QStringList getProvidersFromMCCMNC(const QString &mccmnc); + QString getGsmNumber() const + { + return QString("*99#"); + } + QString getCdmaNumber() const + { + return QString("#777"); + } + inline ErrorCodes getError() + { + return mError; + } + +private: + ProviderData parseProvider(const QDomNode &providerNode); + + QHash mCountries; + QHash mMccMncToName; + QMap mProvidersGsm; + QMap mProvidersCdma; + QMap mApns; + QStringList mNetworkIds; + QDomDocument mDocProviders; + QDomElement docElement; + ErrorCodes mError; + QString getNameByLocale(const QMap &names) const; +}; + +#endif // PLASMA_NM_MOBILE_PROVIDERS_H diff --git a/kcms/cellularnetwork/modem.cpp b/kcms/cellularnetwork/modem.cpp new file mode 100644 index 00000000..ab5d9a58 --- /dev/null +++ b/kcms/cellularnetwork/modem.cpp @@ -0,0 +1,527 @@ +// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "modem.h" + +#include + +#include +#include + +Modem::Modem(QObject *parent) + : QObject{parent} +{ +} + +Modem::Modem(QObject *parent, ModemManager::ModemDevice::Ptr mmModem, ModemManager::Modem::Ptr mmInterface) + : QObject{parent} + , m_mmModem{mmModem} + , m_nmModem{nullptr} + , m_mmInterface{mmInterface} +{ + // TODO multi-sim support + m_sims = {new Sim{this, this, m_mmModem->sim(), m_mmInterface, m_mm3gppDevice}}; + + connect(m_mmModem.data(), &ModemManager::ModemDevice::simAdded, this, &Modem::simsChanged); + connect(m_mmModem.data(), &ModemManager::ModemDevice::simAdded, this, &Modem::hasSimChanged); + connect(m_mmModem.data(), &ModemManager::ModemDevice::simRemoved, this, &Modem::simsChanged); + connect(m_mmModem.data(), &ModemManager::ModemDevice::simRemoved, this, &Modem::hasSimChanged); + + if (m_mmModem->sim()) { + connect(m_mmModem->sim().get(), &ModemManager::Sim::simIdentifierChanged, this, &Modem::hasSimChanged); + } + + connect(NetworkManager::settingsNotifier(), &NetworkManager::SettingsNotifier::connectionAdded, this, &Modem::mobileDataEnabledChanged); + connect(NetworkManager::settingsNotifier(), &NetworkManager::SettingsNotifier::connectionRemoved, this, &Modem::mobileDataEnabledChanged); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::activeConnectionAdded, this, &Modem::mobileDataEnabledChanged); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::activeConnectionRemoved, this, &Modem::mobileDataEnabledChanged); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::deviceAdded, this, &Modem::findNetworkManagerDevice); + connect(NetworkManager::notifier(), &NetworkManager::Notifier::deviceRemoved, this, &Modem::findNetworkManagerDevice); + + // this is guaranteed to be a GSM modem + m_mm3gppDevice = m_mmModem->interface(ModemManager::ModemDevice::GsmInterface).objectCast(); + + // if no sim is inserted, m_mm3gppDevice is nullptr + if (m_mm3gppDevice) { + m_mm3gppDevice->setTimeout(60000); // scanning networks likely takes longer than the default timeout + } + + // find networkmanager modem, if it exists + findNetworkManagerDevice(); + + // we need to initialize it after m_mm3gppDevice has been set + m_details = new ModemDetails(this, this); +} + +void Modem::findNetworkManagerDevice() +{ + m_nmModem = nullptr; + + // find networkmanager modem device + for (NetworkManager::Device::Ptr nmDevice : NetworkManager::networkInterfaces()) { + if (nmDevice->udi() == m_mmModem->uni()) { + m_nmModem = nmDevice.objectCast(); + } + } + + if (m_nmModem) { + connect(m_nmModem.data(), &NetworkManager::Device::autoconnectChanged, this, &Modem::mobileDataEnabledChanged); + connect(m_nmModem.data(), &NetworkManager::Device::stateChanged, this, &Modem::mobileDataEnabledChanged); + connect(m_nmModem.data(), &NetworkManager::Device::availableConnectionAppeared, this, &Modem::mobileDataEnabledChanged); + connect(m_nmModem.data(), &NetworkManager::Device::availableConnectionDisappeared, this, &Modem::mobileDataEnabledChanged); + + connect(m_nmModem.data(), &NetworkManager::ModemDevice::availableConnectionChanged, this, [this]() -> void { + refreshProfiles(); + }); + connect(m_nmModem.data(), &NetworkManager::ModemDevice::activeConnectionChanged, this, [this]() -> void { + refreshProfiles(); + Q_EMIT activeConnectionUniChanged(); + }); + connect(m_nmModem.data(), &NetworkManager::ModemDevice::stateChanged, this, [this](auto newstate, auto oldstate, auto reason) -> void { + qDebug() << QStringLiteral("Modem") << m_nmModem->uni() << QStringLiteral("changed state:") << nmDeviceStateStr(oldstate) << QStringLiteral("->") + << nmDeviceStateStr(newstate) << QStringLiteral("due to:") << reason; + }); + + // add connection profiles + refreshProfiles(); + } + + Q_EMIT nmModemChanged(); + Q_EMIT mobileDataEnabledChanged(); + Q_EMIT mobileDataSupportedChanged(); +} + +ModemDetails *Modem::modemDetails() const +{ + return m_details; +} + +QString Modem::displayId() const +{ + // in the form /org/freedesktop/ModemManager1/Modem/0 + QStringList uniSplit = uni().split("/"); + return uniSplit.count() == 0 ? QStringLiteral("(empty)") : QString(uniSplit[uniSplit.size() - 1]); +} + +QString Modem::uni() const +{ + return m_mmInterface->uni(); +} + +QString Modem::activeConnectionUni() const +{ + if (m_nmModem && m_nmModem->activeConnection() && m_nmModem->activeConnection()->connection()) { + return m_nmModem->activeConnection()->connection()->uuid(); + } + return QString(); +} + +void Modem::reset() +{ + qDebug() << QStringLiteral("Resetting the modem..."); + QDBusPendingReply reply = m_mmInterface->reset(); + reply.waitForFinished(); + if (reply.isError()) { + qDebug() << QStringLiteral("Error resetting the modem:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error resetting the modem: %1", reply.error().message())); + } +} + +bool Modem::mobileDataSupported() const +{ + return m_nmModem && hasSim(); +} + +bool Modem::needsAPNAdded() const +{ + return m_nmModem && mobileDataSupported() && m_nmModem->availableConnections().count() == 0; +} + +bool Modem::mobileDataEnabled() const +{ + // no modem -> no mobile data -> report disabled + if (!m_nmModem) { + return false; + } + + // mobile data already activated -> report enabled + if (m_nmModem->state() == NetworkManager::Device::Activated) { + return true; + } + + // autoconnect disabled on the entire modem -> report disabled + if (!m_nmModem->autoconnect()) { + return false; + } + + // at least one connection set to autoconnect -> report enabled + for (NetworkManager::Connection::Ptr con : m_nmModem->availableConnections()) { + if (con->settings()->autoconnect()) { + return true; + } + } + + // modem, but no connection, set to autoconnect -> report disabled + return false; +} + +void Modem::setMobileDataEnabled(bool enabled) +{ + if (!m_nmModem) { + return; + } + + if (!enabled) { + m_nmModem->setAutoconnect(false); + // we need to also set all connections to not autoconnect (#182) + for (NetworkManager::Connection::Ptr con : m_nmModem->availableConnections()) { + con->settings()->setAutoconnect(false); + con->update(con->settings()->toMap()); + } + m_nmModem->disconnectInterface(); + } else { + m_nmModem->setAutoconnect(true); + // activate the connection that was last used + QDateTime latestTimestamp; + NetworkManager::Connection::Ptr latestCon; + for (NetworkManager::Connection::Ptr con : m_nmModem->availableConnections()) { + QDateTime timestamp = con->settings()->timestamp(); + // if con was not used yet, skip it, otherwise: + // if we have no latestTimestamp yet, con is the latest + // otherwise, compare the timestamps + // in case of a tie, use the first connection that was found + if (!timestamp.isNull() && (latestTimestamp.isNull() || timestamp > latestTimestamp)) { + latestTimestamp = timestamp; + latestCon = con; + } + } + // if we found the last used connection + if (!latestCon.isNull()) { + // set it to autoconnect and connect it immediately + latestCon->settings()->setAutoconnect(true); + latestCon->update(latestCon->settings()->toMap()); + NetworkManager::activateConnection(latestCon->path(), m_nmModem->uni(), ""); + } + } +} + +bool Modem::isRoaming() const +{ + if (!m_nmModem) { + return false; + } + + if (m_nmModem->activeConnection() && m_nmModem->activeConnection()->connection()) { + auto connection = m_nmModem->activeConnection()->connection(); + NetworkManager::GsmSetting::Ptr gsmSetting = connection->settings()->setting(NetworkManager::Setting::Gsm).dynamicCast(); + if (gsmSetting) { + return !gsmSetting->homeOnly(); + } + } + + return false; +} + +void Modem::setIsRoaming(bool roaming) +{ + if (!m_nmModem) { + return; + } + + if (m_nmModem->activeConnection() && m_nmModem->activeConnection()->connection()) { + auto connection = m_nmModem->activeConnection()->connection(); + + NetworkManager::GsmSetting::Ptr gsmSetting = connection->settings()->setting(NetworkManager::Setting::Gsm).dynamicCast(); + if (gsmSetting) { + gsmSetting->setHomeOnly(!roaming); // set roaming setting + + QDBusPendingReply reply = connection->update(connection->settings()->toMap()); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error updating connection settings for") << connection->uuid() << QStringLiteral(":") << reply.error().message() + << QStringLiteral("."); + CellularNetworkSettings::instance()->addMessage( + InlineMessage::Error, + i18n("Error updating connection settings for %1: %2.", connection->uuid(), reply.error().message())); + } else { + qDebug() << QStringLiteral("Successfully updated connection settings") << connection->uuid() << QStringLiteral("."); + } + } + + // the connection uni has changed, refresh the profiles list + refreshProfiles(); + Q_EMIT activeConnectionUniChanged(); + } +} + +bool Modem::hasSim() const +{ + if (!m_mmModem) { + return false; + } + return m_mmModem && m_mmModem->sim() && m_mmModem->sim()->uni() != QStringLiteral("/"); +} + +QList &Modem::profileList() +{ + return m_profileList; +} + +void Modem::refreshProfiles() +{ + m_profileList.clear(); + + if (!m_nmModem) { + Q_EMIT profileListChanged(); + qWarning() << "No NetworkManager modem found, cannot refresh profiles."; + return; + } + + for (auto connection : m_nmModem->availableConnections()) { + for (auto setting : connection->settings()->settings()) { + if (setting.dynamicCast()) { + m_profileList.append(new ProfileSettings(this, setting.dynamicCast(), connection)); + } + } + } + Q_EMIT profileListChanged(); +} + +void Modem::activateProfile(const QString &connectionUni) +{ + if (!m_nmModem) { + qWarning() << "Cannot activate profile since there is no NetworkManager modem"; + return; + } + + qDebug() << QStringLiteral("Activating profile on modem") << m_nmModem->uni() << QStringLiteral("for connection") << connectionUni << "."; + + // cache roaming setting + bool roaming = isRoaming(); + + NetworkManager::Connection::Ptr con; + + // disable autoconnect for all other connections + for (auto connection : m_nmModem->availableConnections()) { + if (connection->uuid() == connectionUni) { + connection->settings()->setAutoconnect(true); + con = connection; + } else { + connection->settings()->setAutoconnect(false); + } + } + + if (!con) { + qDebug() << QStringLiteral("Connection") << connectionUni << QStringLiteral("not found."); + return; + } + + // activate connection manually + // despite the documentation saying otherwise, activateConnection seems to need the DBus path, not uuid of the connection + QDBusPendingReply reply = NetworkManager::activateConnection(con->path(), m_nmModem->uni(), ""); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error activating connection:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error activating connection: %1", reply.error().message())); + return; + } + + // set roaming settings separately (since it changes the uni) + setIsRoaming(roaming); +} + +void Modem::addProfile(QString name, QString apn, QString username, QString password, QString networkType) +{ + if (!m_nmModem) { + qWarning() << "Cannot add profile since there is no NetworkManager modem"; + return; + } + + NetworkManager::ConnectionSettings::Ptr settings{new NetworkManager::ConnectionSettings(NetworkManager::ConnectionSettings::Gsm)}; + settings->setId(name); + settings->setUuid(NetworkManager::ConnectionSettings::createNewUuid()); + settings->setAutoconnect(true); + settings->addToPermissions(KUser().loginName(), QString()); + + NetworkManager::GsmSetting::Ptr gsmSetting = settings->setting(NetworkManager::Setting::Gsm).dynamicCast(); + gsmSetting->setApn(apn); + gsmSetting->setUsername(username); + gsmSetting->setPassword(password); + gsmSetting->setPasswordFlags(password.isEmpty() ? NetworkManager::Setting::NotRequired : NetworkManager::Setting::AgentOwned); + gsmSetting->setNetworkType(ProfileSettings::networkTypeFlag(networkType)); + gsmSetting->setHomeOnly(!isRoaming()); + + gsmSetting->setInitialized(true); + + QDBusPendingReply reply = NetworkManager::addAndActivateConnection(settings->toMap(), m_nmModem->uni(), ""); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error adding connection:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error adding connection: %1", reply.error().message())); + } else { + qDebug() << QStringLiteral("Successfully added a new connection") << name << QStringLiteral("with APN") << apn << "."; + } +} + +void Modem::removeProfile(const QString &connectionUni) +{ + NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni); + if (!con) { + qWarning() << QStringLiteral("Could not find connection") << connectionUni << QStringLiteral("to update!"); + return; + } + + QDBusPendingReply reply = con->remove(); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error removing connection") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error removing connection: %1", reply.error().message())); + } +} + +void Modem::updateProfile(QString connectionUni, QString name, QString apn, QString username, QString password, QString networkType) +{ + NetworkManager::Connection::Ptr con = NetworkManager::findConnectionByUuid(connectionUni); + if (!con) { + qWarning() << QStringLiteral("Could not find connection") << connectionUni << QStringLiteral("to update!"); + return; + } + + NetworkManager::ConnectionSettings::Ptr conSettings = con->settings(); + if (!conSettings) { + qWarning() << QStringLiteral("Could not find connection settings for") << connectionUni << QStringLiteral("to update!"); + return; + } + + conSettings->setId(name); + + NetworkManager::GsmSetting::Ptr gsmSetting = conSettings->setting(NetworkManager::Setting::Gsm).dynamicCast(); + gsmSetting->setApn(apn); + gsmSetting->setUsername(username); + gsmSetting->setPassword(password); + gsmSetting->setPasswordFlags(password == "" ? NetworkManager::Setting::NotRequired : NetworkManager::Setting::AgentOwned); + gsmSetting->setNetworkType(ProfileSettings::networkTypeFlag(networkType)); + gsmSetting->setHomeOnly(!isRoaming()); + + gsmSetting->setInitialized(true); + + QDBusPendingReply reply = con->update(conSettings->toMap()); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error updating connection settings for") << connectionUni << QStringLiteral(":") << reply.error().message() + << QStringLiteral("."); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, + i18n("Error updating connection settings for %1: %2.", connectionUni, reply.error().message())); + } else { + qDebug() << QStringLiteral("Successfully updated connection settings") << connectionUni << QStringLiteral("."); + } +} + +void Modem::addDetectedProfileSettings() +{ + if (!m_mmModem) { + qWarning() << "ModemManager device missing, cannot detect profile settings"; + return; + } + + if (!hasSim()) { + qWarning() << "No SIM found, cannot detect profile settings"; + return; + } + + if (!m_mm3gppDevice) { + qWarning() << "3gpp object not found, cannot detect profile settings"; + return; + } + + bool found = false; + static MobileProviders mobileProviders{}; + + QString operatorCode = m_mm3gppDevice->operatorCode(); + qWarning() << QStringLiteral("Detecting profile settings. Using MCCMNC:") << operatorCode; + + // lookup apns with mccmnc codes + for (QString &provider : mobileProviders.getProvidersFromMCCMNC(operatorCode)) { + qWarning() << QStringLiteral("Provider:") << provider; + + for (auto apn : mobileProviders.getApns(provider)) { + QVariantMap apnInfo = mobileProviders.getApnInfo(apn); + qWarning() << QStringLiteral("Found gsm profile settings. Type:") << apnInfo[QStringLiteral("usageType")]; + + // only add mobile data apns (not mms) + if (apnInfo[QStringLiteral("usageType")].toString() == QStringLiteral("internet")) { + found = true; + + QString name = provider; + if (!apnInfo[QStringLiteral("name")].isNull()) { + name += " - " + apnInfo[QStringLiteral("name")].toString(); + } + + addProfile(name, + apn, + apnInfo[QStringLiteral("username")].toString(), + apnInfo[QStringLiteral("password")].toString(), + QStringLiteral("4G/3G/2G")); + } + + // TODO in the future for MMS settings, add else if here for == "mms" + } + } + + if (!found) { + qDebug() << QStringLiteral("No profiles were found."); + Q_EMIT couldNotAutodetectSettings(); + } +} + +QList Modem::sims() +{ + return m_sims; +} + +ModemManager::ModemDevice::Ptr Modem::mmModemDevice() +{ + return m_mmModem; +} + +NetworkManager::ModemDevice::Ptr Modem::nmModemDevice() +{ + return m_nmModem; +} + +ModemManager::Modem::Ptr Modem::mmModemInterface() +{ + return m_mmInterface; +} + +QString Modem::nmDeviceStateStr(NetworkManager::Device::State state) +{ + if (state == NetworkManager::Device::State::UnknownState) + return i18n("Unknown"); + else if (state == NetworkManager::Device::State::Unmanaged) + return i18n("Unmanaged"); + else if (state == NetworkManager::Device::State::Unavailable) + return i18n("Unavailable"); + else if (state == NetworkManager::Device::State::Disconnected) + return i18n("Disconnected"); + else if (state == NetworkManager::Device::State::Preparing) + return i18n("Preparing"); + else if (state == NetworkManager::Device::State::ConfiguringHardware) + return i18n("ConfiguringHardware"); + else if (state == NetworkManager::Device::State::NeedAuth) + return i18n("NeedAuth"); + else if (state == NetworkManager::Device::State::ConfiguringIp) + return i18n("ConfiguringIp"); + else if (state == NetworkManager::Device::State::CheckingIp) + return i18n("CheckingIp"); + else if (state == NetworkManager::Device::State::WaitingForSecondaries) + return i18n("WaitingForSecondaries"); + else if (state == NetworkManager::Device::State::Activated) + return i18n("Activated"); + else if (state == NetworkManager::Device::State::Deactivating) + return i18n("Deactivating"); + else if (state == NetworkManager::Device::State::Failed) + return i18n("Failed"); + else + return ""; +} diff --git a/kcms/cellularnetwork/modem.h b/kcms/cellularnetwork/modem.h new file mode 100644 index 00000000..2ad91c3f --- /dev/null +++ b/kcms/cellularnetwork/modem.h @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "cellularnetworksettings.h" +#include "modemdetails.h" +#include "profilesettings.h" +#include "sim.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class ProfileSettings; +class Sim; +class AvailableNetwork; +class ModemDetails; +class MobileProviders; + +// only supports GSM/UMTS/LTE +class Modem : public QObject +{ + Q_OBJECT + Q_PROPERTY(ModemDetails *details READ modemDetails NOTIFY modemDetailsChanged) + Q_PROPERTY(QString uni READ uni NOTIFY uniChanged) + Q_PROPERTY(QString displayId READ displayId NOTIFY displayIdChanged) + + Q_PROPERTY(bool isRoaming READ isRoaming WRITE setIsRoaming NOTIFY isRoamingChanged) + Q_PROPERTY(bool hasSim READ hasSim NOTIFY hasSimChanged) + Q_PROPERTY(QList profiles READ profileList NOTIFY profileListChanged) + Q_PROPERTY(QString activeConnectionUni READ activeConnectionUni NOTIFY activeConnectionUniChanged) + + Q_PROPERTY(bool mobileDataEnabled READ mobileDataEnabled WRITE setMobileDataEnabled NOTIFY mobileDataEnabledChanged) + Q_PROPERTY(bool mobileDataSupported READ mobileDataSupported NOTIFY mobileDataSupportedChanged) + Q_PROPERTY(bool needsAPNAdded READ needsAPNAdded NOTIFY mobileDataEnabledChanged) + +public: + Modem(QObject *parent = nullptr); + Modem(QObject *parent, ModemManager::ModemDevice::Ptr mmModem, ModemManager::Modem::Ptr m_mmInterface); + + ModemDetails *modemDetails() const; + QString displayId() const; // splits uni and obtains the number suffix + QString uni() const; + QString activeConnectionUni() const; + + Q_INVOKABLE void reset(); + + bool mobileDataEnabled() const; + void setMobileDataEnabled(bool enabled); + bool mobileDataSupported() const; + bool needsAPNAdded() const; + + bool isRoaming() const; + void setIsRoaming(bool roaming); + bool hasSim() const; + + // connection profiles + QList &profileList(); + void refreshProfiles(); + Q_INVOKABLE void activateProfile(const QString &connectionUni); + Q_INVOKABLE void addProfile(QString name, QString apn, QString username, QString password, QString networkType); + Q_INVOKABLE void removeProfile(const QString &connectionUni); + Q_INVOKABLE void updateProfile(QString connectionUni, QString name, QString apn, QString username, QString password, QString networkType); + Q_INVOKABLE void addDetectedProfileSettings(); // detect modem connection settings (ex. apn) and add a new connection + + QList sims(); + + ModemManager::ModemDevice::Ptr mmModemDevice(); + NetworkManager::ModemDevice::Ptr nmModemDevice(); + ModemManager::Modem::Ptr mmModemInterface(); + +Q_SIGNALS: + void modemDetailsChanged(); + void uniChanged(); + void displayIdChanged(); + void activeConnectionUniChanged(); + + void nmModemChanged(); + + void mobileDataEnabledChanged(); + void mobileDataSupportedChanged(); + void isRoamingChanged(); + void simsChanged(); + void hasSimChanged(); + void profileListChanged(); + + void couldNotAutodetectSettings(); + +private: + void findNetworkManagerDevice(); + + QString nmDeviceStateStr(NetworkManager::Device::State state); + + ModemDetails *m_details; + + ModemManager::ModemDevice::Ptr m_mmModem; + NetworkManager::ModemDevice::Ptr m_nmModem; // may be a nullptr if the nm modem hasn't been found yet + ModemManager::Modem::Ptr m_mmInterface = nullptr; + ModemManager::Modem3gpp::Ptr m_mm3gppDevice = nullptr; // this may be a nullptr if no sim is inserted + + QList m_sims; + QList m_profileList; + + friend class ModemDetails; +}; diff --git a/kcms/cellularnetwork/modemdetails.cpp b/kcms/cellularnetwork/modemdetails.cpp new file mode 100644 index 00000000..e28c81e8 --- /dev/null +++ b/kcms/cellularnetwork/modemdetails.cpp @@ -0,0 +1,547 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#include "modemdetails.h" + +#include + +#include + +ModemDetails::ModemDetails(QObject *parent, Modem *modem) + : QObject{parent} + , m_modem{modem} + , m_scanNetworkWatcher{nullptr} + , m_isScanningNetworks{false} + , m_cachedScannedNetworks{} +{ + auto mmInterfacePointer = m_modem->m_mmInterface.data(); + connect(mmInterfacePointer, &ModemManager::Modem::accessTechnologiesChanged, this, [this]() -> void { + Q_EMIT accessTechnologiesChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::deviceChanged, this, [this]() -> void { + Q_EMIT deviceChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::deviceIdentifierChanged, this, [this]() -> void { + Q_EMIT deviceIdentifierChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::driversChanged, this, [this]() -> void { + Q_EMIT driversChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::equipmentIdentifierChanged, this, [this]() -> void { + Q_EMIT equipmentIdentifierChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::manufacturerChanged, this, [this]() -> void { + Q_EMIT manufacturerChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::modelChanged, this, [this]() -> void { + Q_EMIT modelChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::ownNumbersChanged, this, [this]() -> void { + Q_EMIT ownNumbersChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::pluginChanged, this, [this]() -> void { + Q_EMIT pluginChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::powerStateChanged, this, [this]() -> void { + Q_EMIT powerStateChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::revisionChanged, this, [this]() -> void { + Q_EMIT revisionChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::signalQualityChanged, this, [this]() -> void { + Q_EMIT signalQualityChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::simPathChanged, this, [this]() -> void { + Q_EMIT simPathChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::stateChanged, this, [this]() -> void { + Q_EMIT stateChanged(); + }); + connect(mmInterfacePointer, &ModemManager::Modem::stateFailedReasonChanged, this, [this]() -> void { + Q_EMIT stateFailedReasonChanged(); + }); + + if (m_modem->m_mm3gppDevice) { + connect(m_modem->m_mm3gppDevice.data(), &ModemManager::Modem3gpp::operatorCodeChanged, this, [this]() -> void { + Q_EMIT operatorCodeChanged(); + }); + connect(m_modem->m_mm3gppDevice.data(), &ModemManager::Modem3gpp::operatorNameChanged, this, [this]() -> void { + Q_EMIT operatorNameChanged(); + }); + connect(m_modem->m_mm3gppDevice.data(), &ModemManager::Modem3gpp::registrationStateChanged, this, [this]() -> void { + Q_EMIT registrationStateChanged(); + Q_EMIT m_modem->isRoamingChanged(); + }); + } else { + qWarning() << QStringLiteral("3gpp device not found!"); + } + + // m_modem->m_nmModem may be nullptr, listen for updates + connect(m_modem, &Modem::nmModemChanged, this, &ModemDetails::updateNMSignals); + updateNMSignals(); +} + +void ModemDetails::updateNMSignals() +{ + if (m_modem->m_nmModem) { + connect(m_modem->m_nmModem.data(), &NetworkManager::ModemDevice::firmwareVersionChanged, this, [this]() -> void { + Q_EMIT firmwareVersionChanged(); + }); + connect(m_modem->m_nmModem.data(), &NetworkManager::ModemDevice::interfaceNameChanged, this, [this]() -> void { + Q_EMIT interfaceNameChanged(); + }); + connect(m_modem->m_nmModem.data(), &NetworkManager::ModemDevice::meteredChanged, this, [this]() -> void { + Q_EMIT meteredChanged(); + }); + } +} + +ModemDetails &ModemDetails::operator=(ModemDetails &&other) +{ + swap(other); + return *this; +} + +void ModemDetails::swap(ModemDetails &other) +{ + std::swap(m_modem, other.m_modem); + std::swap(m_cachedScannedNetworks, other.m_cachedScannedNetworks); + std::swap(m_isScanningNetworks, other.m_isScanningNetworks); + std::swap(m_scanNetworkWatcher, other.m_scanNetworkWatcher); +} + +QStringList ModemDetails::accessTechnologies() +{ + QStringList list; + auto flags = m_modem->m_mmInterface->accessTechnologies(); + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN) { + list.push_back(i18n("Unknown")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_POTS) { + list.push_back(i18n("POTS")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_GSM) { + list.push_back(i18n("GSM")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT) { + list.push_back(i18n("GSM Compact")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_GPRS) { + list.push_back(i18n("GPRS")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_EDGE) { + list.push_back(i18n("EDGE")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_UMTS) { + list.push_back(i18n("UMTS")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_HSDPA) { + list.push_back(i18n("HSDPA")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_HSUPA) { + list.push_back(i18n("HSUPA")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_HSPA) { + list.push_back(i18n("HSPA")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS) { + list.push_back(i18n("HSPA+")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_1XRTT) { + list.push_back(i18n("CDMA2000 1xRTT")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_EVDO0) { + list.push_back(i18n("CDMA2000 EVDO-0")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_EVDOA) { + list.push_back(i18n("CDMA2000 EVDO-A")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_EVDOB) { + list.push_back(i18n("CDMA2000 EVDO-B")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_LTE) { + list.push_back(i18n("LTE")); + } + if (flags & MM_MODEM_ACCESS_TECHNOLOGY_5GNR) { + list.push_back(i18n("5GNR")); + } + return list; +} + +QString ModemDetails::device() +{ + return m_modem->m_mmInterface->device(); +} + +QString ModemDetails::deviceIdentifier() +{ + return m_modem->m_mmInterface->deviceIdentifier(); +} + +QStringList ModemDetails::drivers() +{ + return m_modem->m_mmInterface->drivers(); +} + +QString ModemDetails::equipmentIdentifier() +{ + return m_modem->m_mmInterface->equipmentIdentifier(); +} + +bool ModemDetails::isEnabled() +{ + return m_modem->m_mmInterface->isEnabled(); +} + +QString ModemDetails::manufacturer() +{ + return m_modem->m_mmInterface->manufacturer(); +} + +QString ModemDetails::model() +{ + return m_modem->m_mmInterface->model(); +} + +QStringList ModemDetails::ownNumbers() +{ + return m_modem->m_mmInterface->ownNumbers(); +} + +QString ModemDetails::plugin() +{ + return m_modem->m_mmInterface->plugin(); +} + +QString ModemDetails::powerState() +{ + switch (m_modem->m_mmInterface->powerState()) { + case MM_MODEM_POWER_STATE_UNKNOWN: + return i18n("Unknown"); + case MM_MODEM_POWER_STATE_OFF: + return i18n("Off"); + case MM_MODEM_POWER_STATE_LOW: + return i18n("Low-power mode"); + case MM_MODEM_POWER_STATE_ON: + return i18n("Full power mode"); + } + return {}; +} + +QString ModemDetails::revision() +{ + return m_modem->m_mmInterface->revision(); +} + +uint ModemDetails::signalQuality() +{ + return m_modem->m_mmInterface->signalQuality().signal; +} + +QString ModemDetails::simPath() +{ + return m_modem->m_mmInterface->simPath(); +} + +QString ModemDetails::state() +{ + switch (m_modem->m_mmInterface->state()) { + case MM_MODEM_STATE_FAILED: + return i18n("Failed"); + case MM_MODEM_STATE_UNKNOWN: + return i18n("Unknown"); + case MM_MODEM_STATE_INITIALIZING: + return i18n("Initializing"); + case MM_MODEM_STATE_LOCKED: + return i18n("Locked"); + case MM_MODEM_STATE_DISABLED: + return i18n("Disabled"); + case MM_MODEM_STATE_DISABLING: + return i18n("Disabling"); + case MM_MODEM_STATE_ENABLING: + return i18n("Enabling"); + case MM_MODEM_STATE_ENABLED: + return i18n("Enabled"); + case MM_MODEM_STATE_SEARCHING: + return i18n("Searching for network provider"); + case MM_MODEM_STATE_REGISTERED: + return i18n("Registered with network provider"); + case MM_MODEM_STATE_DISCONNECTING: + return i18n("Disconnecting"); + case MM_MODEM_STATE_CONNECTING: + return i18n("Connecting"); + case MM_MODEM_STATE_CONNECTED: + return i18n("Connected"); + } + return {}; +} + +QString ModemDetails::stateFailedReason() +{ + switch (m_modem->m_mmInterface->stateFailedReason()) { + case MM_MODEM_STATE_FAILED_REASON_NONE: + return i18n("No error."); + case MM_MODEM_STATE_FAILED_REASON_UNKNOWN: + return i18n("Unknown error."); + case MM_MODEM_STATE_FAILED_REASON_SIM_MISSING: + return i18n("SIM is required but missing."); + case MM_MODEM_STATE_FAILED_REASON_SIM_ERROR: + return i18n("SIM is available but unusable."); + } + return {}; +} + +QString ModemDetails::operatorCode() +{ + return m_modem->m_mm3gppDevice ? m_modem->m_mm3gppDevice->operatorCode() : QString{}; +} + +QString ModemDetails::operatorName() +{ + return m_modem->m_mm3gppDevice ? m_modem->m_mm3gppDevice->operatorName() : QString{}; +} + +QString ModemDetails::registrationState() +{ + if (!m_modem->m_mm3gppDevice) { + return QString{}; + } + + switch (m_modem->m_mm3gppDevice->registrationState()) { + case MM_MODEM_3GPP_REGISTRATION_STATE_IDLE: + return i18n("Not registered, not searching for new operator to register."); + case MM_MODEM_3GPP_REGISTRATION_STATE_HOME: + return i18n("Registered on home network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_SEARCHING: + return i18n("Not registered, searching for new operator to register with."); + case MM_MODEM_3GPP_REGISTRATION_STATE_DENIED: + return i18n("Registration denied."); + case MM_MODEM_3GPP_REGISTRATION_STATE_UNKNOWN: + return i18n("Unknown registration status."); + case MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING: + return i18n("Registered on a roaming network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_HOME_SMS_ONLY: + return i18n("Registered for \"SMS only\", on home network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_SMS_ONLY: + return i18n("Registered for \"SMS only\", roaming network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_EMERGENCY_ONLY: + return i18n("Emergency services only."); + case MM_MODEM_3GPP_REGISTRATION_STATE_HOME_CSFB_NOT_PREFERRED: + return i18n("Registered for \"CSFB not preferred\", home network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_ROAMING_CSFB_NOT_PREFERRED: + return i18n("Registered for \"CSFB not preferred\", roaming network."); + case MM_MODEM_3GPP_REGISTRATION_STATE_ATTACHED_RLOS: + return i18n("Attached for access to Restricted Local Operator Services."); + } + return {}; +} + +Q_DECLARE_METATYPE(MMModem3gppNetworkAvailability) +Q_DECLARE_METATYPE(MMModemAccessTechnology) + +QList ModemDetails::networks() +{ + return m_cachedScannedNetworks; +} + +Q_INVOKABLE void ModemDetails::scanNetworks() +{ + for (auto p : m_cachedScannedNetworks) { + p->deleteLater(); + } + m_cachedScannedNetworks.clear(); + + if (m_modem->m_mm3gppDevice) { + qDebug() << QStringLiteral("Scanning for available networks..."); + + QDBusPendingReply reply = m_modem->m_mm3gppDevice->scan(); + + m_isScanningNetworks = true; + Q_EMIT isScanningNetworksChanged(); + m_scanNetworkWatcher = new QDBusPendingCallWatcher(reply, this); + connect(m_scanNetworkWatcher, &QDBusPendingCallWatcher::finished, this, &ModemDetails::scanNetworksFinished); + } + + Q_EMIT networksChanged(); +} + +void ModemDetails::scanNetworksFinished(QDBusPendingCallWatcher *call) +{ + QDBusPendingReply reply = *call; + if (reply.isError()) { + qDebug() << QStringLiteral("Scanning failed:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Scanning networks failed: %1", reply.error().message())); + } else { + ModemManager::QVariantMapList list = reply.value(); + + for (auto &var : list) { + auto status = var[QStringLiteral("status")].value(); + + if (status == MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT || status == MM_MODEM_3GPP_NETWORK_AVAILABILITY_AVAILABLE) { + auto network = new AvailableNetwork{this, + m_modem->m_mm3gppDevice, + status == MM_MODEM_3GPP_NETWORK_AVAILABILITY_CURRENT, + var[QStringLiteral("operator-long")].toString(), + var[QStringLiteral("operator-short")].toString(), + var[QStringLiteral("operator-code")].toString(), + var[QStringLiteral("access-technology")].value()}; + m_cachedScannedNetworks.push_back(network); + } + } + } + m_isScanningNetworks = false; + Q_EMIT networksChanged(); + Q_EMIT isScanningNetworksChanged(); + + call->deleteLater(); +} + +bool ModemDetails::isScanningNetworks() +{ + return m_isScanningNetworks; +} + +QString ModemDetails::firmwareVersion() +{ + if (!m_modem->m_nmModem) { + return QString{}; + } + return m_modem->m_nmModem->firmwareVersion(); +} + +QString ModemDetails::interfaceName() +{ + if (!m_modem->m_nmModem) { + return QString{}; + } + return m_modem->m_nmModem->interfaceName(); +} + +QString ModemDetails::metered() +{ + if (!m_modem->m_nmModem) { + return QString{}; + } + + switch (m_modem->m_nmModem->metered()) { + case NetworkManager::Device::MeteredStatus::UnknownStatus: + return i18n("Unknown"); + case NetworkManager::Device::MeteredStatus::Yes: + return i18n("Yes"); + case NetworkManager::Device::MeteredStatus::No: + return i18n("No"); + case NetworkManager::Device::MeteredStatus::GuessYes: + return i18n("GuessYes"); + case NetworkManager::Device::MeteredStatus::GuessNo: + return i18n("GuessNo"); + } + return QString{}; +} + +AvailableNetwork::AvailableNetwork(QObject *parent, + ModemManager::Modem3gpp::Ptr mm3gppDevice, + bool isCurrentlyUsed, + QString operatorLong, + QString operatorShort, + QString operatorCode, + MMModemAccessTechnology accessTechnology) + : QObject{parent} + , m_isCurrentlyUsed{isCurrentlyUsed} + , m_operatorLong{operatorLong} + , m_operatorShort{operatorShort} + , m_operatorCode{operatorCode} + , m_accessTechnology{} + , m_mm3gppDevice{mm3gppDevice} +{ + switch (accessTechnology) { + case MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN: + m_accessTechnology = i18n("Unknown"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_POTS: + m_accessTechnology = i18n("POTS"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_GSM: + m_accessTechnology = i18n("2G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT: + m_accessTechnology = i18n("2G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_GPRS: + m_accessTechnology = i18n("2G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_EDGE: + m_accessTechnology = i18n("2G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_UMTS: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_HSDPA: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_HSUPA: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_HSPA: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_1XRTT: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_EVDO0: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_EVDOA: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_EVDOB: + m_accessTechnology = i18n("3G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_LTE: + m_accessTechnology = i18n("4G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_5GNR: + m_accessTechnology = i18n("5G"); + break; + case MM_MODEM_ACCESS_TECHNOLOGY_ANY: + m_accessTechnology = i18n("Any"); + break; + } +} + +bool AvailableNetwork::isCurrentlyUsed() +{ + return m_isCurrentlyUsed; +} + +QString AvailableNetwork::operatorLong() +{ + return m_operatorLong; +} + +QString AvailableNetwork::operatorShort() +{ + return m_operatorShort; +} + +QString AvailableNetwork::operatorCode() +{ + return m_operatorCode; +} + +QString AvailableNetwork::accessTechnology() +{ + return m_accessTechnology; +} + +void AvailableNetwork::registerToNetwork() +{ + if (!m_isCurrentlyUsed && m_mm3gppDevice) { + m_mm3gppDevice->registerToNetwork(m_operatorCode); + } +} diff --git a/kcms/cellularnetwork/modemdetails.h b/kcms/cellularnetwork/modemdetails.h new file mode 100644 index 00000000..1facbbb6 --- /dev/null +++ b/kcms/cellularnetwork/modemdetails.h @@ -0,0 +1,181 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#pragma once + +#include "modem.h" +#include "sim.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class Modem; +class AvailableNetwork; + +class ModemDetails : public QObject +{ + Q_OBJECT + // modemmanager device + Q_PROPERTY(QStringList accessTechnologies READ accessTechnologies NOTIFY accessTechnologiesChanged) // currently used tech + Q_PROPERTY(QString device READ device NOTIFY deviceChanged) + Q_PROPERTY(QString deviceIdentifier READ deviceIdentifier NOTIFY deviceIdentifierChanged) + Q_PROPERTY(QStringList drivers READ drivers NOTIFY driversChanged) + Q_PROPERTY(QString equipmentIdentifier READ equipmentIdentifier NOTIFY equipmentIdentifierChanged) + // TODO add bands + Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY isEnabledChanged) + Q_PROPERTY(QString manufacturer READ manufacturer NOTIFY manufacturerChanged) + Q_PROPERTY(QString model READ model NOTIFY modelChanged) + Q_PROPERTY(QStringList ownNumbers READ ownNumbers NOTIFY ownNumbersChanged) + Q_PROPERTY(QString plugin READ plugin NOTIFY pluginChanged) + Q_PROPERTY(QString powerState READ powerState NOTIFY powerStateChanged) + Q_PROPERTY(QString revision READ revision NOTIFY revisionChanged) + Q_PROPERTY(uint signalQuality READ signalQuality NOTIFY signalQualityChanged) + Q_PROPERTY(QString simPath READ simPath NOTIFY simPathChanged) + Q_PROPERTY(QString state READ state NOTIFY stateChanged) + Q_PROPERTY(QString stateFailedReason READ stateFailedReason NOTIFY stateFailedReasonChanged) + + // modemmanager 3gpp device + Q_PROPERTY(QString operatorCode READ operatorCode NOTIFY operatorCodeChanged) + Q_PROPERTY(QString operatorName READ operatorName NOTIFY operatorNameChanged) + Q_PROPERTY(QString registrationState READ registrationState NOTIFY registrationStateChanged) + Q_PROPERTY(QList networks READ networks NOTIFY networksChanged) + Q_PROPERTY(bool isScanningNetworks READ isScanningNetworks NOTIFY isScanningNetworksChanged) + + // networkmanager device + Q_PROPERTY(QString firmwareVersion READ firmwareVersion NOTIFY firmwareVersionChanged) + Q_PROPERTY(QString interfaceName READ interfaceName NOTIFY interfaceNameChanged) + Q_PROPERTY(QString metered READ metered NOTIFY meteredChanged) + +public: + ModemDetails(QObject *parent = nullptr, Modem *modem = nullptr); + ModemDetails &operator=(ModemDetails &&other); + void swap(ModemDetails &other); + + QStringList accessTechnologies(); + QString device(); + QString deviceIdentifier(); + QStringList drivers(); + QString equipmentIdentifier(); + bool isEnabled(); + QString manufacturer(); + uint maxActiveBearers(); + uint maxBearers(); + QString model(); + QStringList ownNumbers(); + QString plugin(); + QString powerState(); + QString revision(); + uint signalQuality(); + QString simPath(); + QString state(); + QString stateFailedReason(); + + QString operatorCode(); + QString operatorName(); + QString registrationState(); + + Q_INVOKABLE void scanNetworks(); + QList networks(); + bool isScanningNetworks(); + void scanNetworksFinished(QDBusPendingCallWatcher *call); + + QString firmwareVersion(); + QString interfaceName(); + QString metered(); + +Q_SIGNALS: + void accessTechnologiesChanged(); + void deviceChanged(); + void deviceIdentifierChanged(); + void driversChanged(); + void equipmentIdentifierChanged(); + void isEnabledChanged(); + void manufacturerChanged(); + void modelChanged(); + void ownNumbersChanged(); + void pluginChanged(); + void powerStateChanged(); + void revisionChanged(); + void signalQualityChanged(); + void simPathChanged(); + void stateChanged(); + void stateFailedReasonChanged(); + void supportedCapabilitiesChanged(); + + void operatorCodeChanged(); + void operatorNameChanged(); + void registrationStateChanged(); + void networksChanged(); + void isScanningNetworksChanged(); + + void firmwareVersionChanged(); + void interfaceNameChanged(); + void meteredChanged(); + +private: + void updateNMSignals(); + + Modem *m_modem; + + QDBusPendingCallWatcher *m_scanNetworkWatcher; + bool m_isScanningNetworks; + QList m_cachedScannedNetworks; +}; + +class AvailableNetwork : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool isCurrentlyUsed READ isCurrentlyUsed NOTIFY isCurrentlyUsedChanged) + Q_PROPERTY(QString operatorLong READ operatorLong NOTIFY operatorLongChanged) + Q_PROPERTY(QString operatorShort READ operatorShort NOTIFY operatorShortChanged) + Q_PROPERTY(QString operatorCode READ operatorCode NOTIFY operatorCodeChanged) + Q_PROPERTY(QString accessTechnology READ accessTechnology NOTIFY accessTechnologyChanged) + +public: + AvailableNetwork(QObject *parent = nullptr, + ModemManager::Modem3gpp::Ptr mm3gppDevice = nullptr, + bool isCurrentlyUsed = false, + QString operatorLong = "", + QString operatorShort = "", + QString operatorCode = "", + MMModemAccessTechnology accessTechnology = MM_MODEM_ACCESS_TECHNOLOGY_UNKNOWN); + + bool isCurrentlyUsed(); + QString operatorLong(); + QString operatorShort(); + QString operatorCode(); + QString accessTechnology(); + + Q_INVOKABLE void registerToNetwork(); + +Q_SIGNALS: + void isCurrentlyUsedChanged(); + void operatorLongChanged(); + void operatorShortChanged(); + void operatorCodeChanged(); + void accessTechnologyChanged(); + +private: + bool m_isCurrentlyUsed; + QString m_operatorLong; + QString m_operatorShort; + QString m_operatorCode; + QString m_accessTechnology; + + ModemManager::Modem3gpp::Ptr m_mm3gppDevice; // this may be a nullptr if no sim is inserted +}; diff --git a/kcms/cellularnetwork/package/contents/ui/AvailableNetworks.qml b/kcms/cellularnetwork/package/contents/ui/AvailableNetworks.qml new file mode 100644 index 00000000..aa1d8153 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/AvailableNetworks.qml @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kcm 1.2 +import cellularnetworkkcm 1.0 + +Kirigami.ScrollablePage { + id: root + title: i18n("Available Networks") + + property Modem modem + property Sim sim + + ListView { + id: listView + header: ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + MessagesList { + visible: count != 0 + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + model: kcm.messages + } + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + visible: !modem.details.isScanningNetworks && listView.count == 0 + icon.name: "network-mobile-100" + text: i18n("Current operator: %1", modem.details.operatorName ? modem.details.operatorName : i18n("none")) + helpfulAction: Kirigami.Action { + icon.name: "view-refresh" + text: i18n("Scan For Networks") + enabled: !modem.details.isScanningNetworks + onTriggered: modem.details.scanNetworks() + } + } + + Controls.BusyIndicator { + anchors.centerIn: parent + visible: modem.details.isScanningNetworks + implicitWidth: Kirigami.Units.iconSizes.large + implicitHeight: implicitWidth + } + + model: modem.details.networks + + delegate: Kirigami.SwipeListItem { + onClicked: { + if (!modelData.isCurrentlyUsed) { + modelData.registerToNetwork(); + modem.details.scanNetworks(); + } + } + + contentItem: RowLayout { + Layout.fillWidth: true + + ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.Heading { + level: 3 + text: modelData.operatorLong + " | " + modelData.operatorShort + "(" + modelData.operatorCode + ")" + } + Controls.Label { + text: modelData.accessTechnology + } + } + Controls.RadioButton { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + checked: modelData.isCurrentlyUsed + onClicked: { + if (!modelData.isCurrentlyUsed) { + modelData.registerToNetwork(); + modem.details.scanNetworks(); + } + checked = modelData.isCurrentlyUsed; + } + } + } + } + } +} + + diff --git a/kcms/cellularnetwork/package/contents/ui/EditProfileDialog.qml b/kcms/cellularnetwork/package/contents/ui/EditProfileDialog.qml new file mode 100644 index 00000000..4ea4f988 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/EditProfileDialog.qml @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2020-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.19 as Kirigami + +import cellularnetworkkcm 1.0 + +Kirigami.Dialog { + id: dialog + title: i18n("Edit APN") + clip: true + + property Modem modem + property ProfileSettings profile + + property int pageWidth + + standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel + + onAccepted: { + if (profile == null) { // create new profile + modem.addProfile(profileName.text, profileApn.text, profileUsername.text, profilePassword.text, profileNetworkType.value); + } else { // edit existing profile + modem.updateProfile(profile.connectionUni, profileName.text, profileApn.text, profileUsername.text, profilePassword.text, profileNetworkType.value); + } + } + preferredWidth: pageWidth - Kirigami.Units.gridUnit * 4 + padding: Kirigami.Units.gridUnit + + ColumnLayout { + Kirigami.FormLayout { + Layout.fillWidth: true + wideMode: false + + Controls.TextField { + id: profileName + Kirigami.FormData.label: i18n("Name") + text: profile != null ? profile.name : "" + } + Controls.TextField { + id: profileApn + Kirigami.FormData.label: i18n("APN") + text: profile != null ? profile.apn : "" + } + Controls.TextField { + id: profileUsername + Kirigami.FormData.label: i18n("Username") + text: profile != null ? profile.user : "" + } + Controls.TextField { + id: profilePassword + Kirigami.FormData.label: i18n("Password") + text: profile != null ? profile.password : "" + } + Controls.ComboBox { + id: profileNetworkType + Kirigami.FormData.label: i18n("Network type") + model: [i18n("4G/3G/2G"), i18n("3G/2G"), i18n("2G"), i18n("Only 4G"), i18n("Only 3G"), i18n("Only 2G"), i18n("Any")] + Component.onCompleted: { + if (profile != null) { + currentIndex = indexOfValue(profile.networkType) + } + } + } + } + } +} diff --git a/kcms/cellularnetwork/package/contents/ui/MessagesList.qml b/kcms/cellularnetwork/package/contents/ui/MessagesList.qml new file mode 100644 index 00000000..b0ca2230 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/MessagesList.qml @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.12 as Kirigami + +import cellularnetworkkcm 1.0 + +ColumnLayout { + id: root + + property var model + property alias count: repeater.count + + spacing: 0 + visible: count > 0 + + Repeater { + id: repeater + model: root.model + + delegate: Kirigami.InlineMessage { + Layout.bottomMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + visible: true + text: modelData.message + type: { + switch (modelData.type) { + case InlineMessage.Information: return Kirigami.MessageType.Information; + case InlineMessage.Positive: return Kirigami.MessageType.Positive; + case InlineMessage.Warning: return Kirigami.MessageType.Warning; + case InlineMessage.Error: return Kirigami.MessageType.Error; + } + return Kirigami.MessageType.Error; + } + + actions: [ + Kirigami.Action { + icon.name: "dialog-close" + onTriggered: kcm.removeMessage(model.index) + } + ] + } + } +} diff --git a/kcms/cellularnetwork/package/contents/ui/ModemPage.qml b/kcms/cellularnetwork/package/contents/ui/ModemPage.qml new file mode 100644 index 00000000..189cc5e1 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/ModemPage.qml @@ -0,0 +1,309 @@ +// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kcm 1.2 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import cellularnetworkkcm 1.0 + +Kirigami.ScrollablePage { + id: modemPage + title: i18n("Modem %1", modem.displayId) + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + property Modem modem + property bool showExtra: false + + ColumnLayout { + MessagesList { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.smallSpacing + visible: count != 0 + model: kcm.messages + } + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Modem Control") + } + + MobileForm.AbstractFormDelegate { + id: modemRestartButton + Layout.fillWidth: true + contentItem: RowLayout { + Controls.Label { + Layout.fillWidth: true + text: i18n("Modem Restart") + } + Controls.Button { + text: i18n("Force Modem Restart") + onClicked: modem.reset() + } + } + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Modem Details") + } + + MobileForm.AbstractFormDelegate { + id: accessTechnologiesText + Layout.fillWidth: true + + background: Item {} + contentItem: ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + Controls.Label { + Layout.fillWidth: true + text: i18n("Access Technologies") + elide: Text.ElideRight + } + Repeater { + model: modem.details.accessTechnologies + Controls.Label { + Layout.fillWidth: true + text: modelData + color: Kirigami.Theme.disabledTextColor + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + } + } + } + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: imeiText + text: i18n("IMEI") + description: modem.details.equipmentIdentifier + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: enabledText + text: i18n("Enabled") + description: modem.details.isEnabled + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: manufacturerText + text: i18n("Manufacturer") + description: modem.details.manufacturer + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: modelText + text: i18n("Model") + description: modem.details.model + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.AbstractFormDelegate { + id: ownedNumbersText + Layout.fillWidth: true + + background: Item {} + contentItem: ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + Controls.Label { + Layout.fillWidth: true + text: i18n("Owned Numbers:") + elide: Text.ElideRight + } + Repeater { + model: modem.details.ownNumbers + Controls.Label { + Layout.fillWidth: true + text: modelData + color: Kirigami.Theme.disabledTextColor + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + } + } + } + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: revisionText + text: i18n("Revision") + description: modem.details.revision + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: signalQualityText + text: i18n("Signal Quality") + description: modem.details.signalQuality + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: stateText + text: i18n("State") + description: modem.details.state + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: failureReasonText + text: i18n("Failure Reason") + description: modem.details.stateFailedReason + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: registrationStateText + text: i18n("Registration State") + description: modem.details.registrationState + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: roamingText + text: i18n("Roaming") + description: modem.isRoaming ? i18n("Yes") : i18n("No") + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: firmwareVersionText + text: i18n("Firmware Version") + description: modem.details.firmwareVersion + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: interfaceNameText + text: i18n("Interface Name") + description: modem.details.interfaceName + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: meteredText + text: i18n("Metered") + description: modem.details.metered + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: activeNMConnectionText + text: i18n("Active NetworkManager Connection") + description: modem.activeConnectionUni + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: deviceText + text: i18n("Device") + description: modem.details.device + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: deviceIdText + text: i18n("Device ID") + description: modem.details.deviceIdentifier + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.AbstractFormDelegate { + id: driversText + Layout.fillWidth: true + + background: Item {} + contentItem: ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + Controls.Label { + Layout.fillWidth: true + text: i18n("Drivers:") + elide: Text.ElideRight + } + Repeater { + model: modem.details.drivers + Controls.Label { + Layout.fillWidth: true + text: modelData + color: Kirigami.Theme.disabledTextColor + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + } + } + } + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: pluginText + text: i18n("Plugin") + description: modem.details.plugin + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: powerStateText + text: i18n("Power State") + description: modem.details.powerState + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: simPathText + text: i18n("SIM Path") + description: modem.details.simPath + } + } + } + } +} + diff --git a/kcms/cellularnetwork/package/contents/ui/PopupDialog.qml b/kcms/cellularnetwork/package/contents/ui/PopupDialog.qml new file mode 100644 index 00000000..f3d2497f --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/PopupDialog.qml @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.15 as Controls +import QtGraphicalEffects 1.12 +import org.kde.kirigami 2.12 as Kirigami + +Controls.Dialog { + id: dialog + + anchors.centerIn: Controls.Overlay.overlay + modal: true + padding: Kirigami.Units.smallSpacing + closePolicy: Controls.Popup.CloseOnEscape | Controls.Popup.CloseOnReleaseOutside + + property int translateY: (1 - opacity) * Kirigami.Units.gridUnit * 2 + + NumberAnimation on opacity { + from: 0; to: 1; + duration: Kirigami.Units.veryShortDuration + easing.type: Easing.InOutQuad + running: true + } + + contentItem.transform: Translate { y: dialog.translateY } + footer.transform: Translate { y: dialog.translateY } + + header: Item { + transform: Translate { y: dialog.translateY } + implicitHeight: heading.implicitHeight + Kirigami.Units.largeSpacing * 2 + + Kirigami.Heading { + id: heading + level: 2 + text: dialog.title + elide: Text.ElideRight + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Kirigami.Units.largeSpacing + anchors.verticalCenter: parent.verticalCenter + + // use tooltip for long text that is elided + Controls.ToolTip.visible: truncated && titleHoverHandler.hovered + Controls.ToolTip.text: dialog.title + HoverHandler { + id: titleHoverHandler + } + } + } + + background: Item { + transform: Translate { y: dialog.translateY } + + RectangularGlow { + anchors.fill: rect + anchors.topMargin: 1 + cornerRadius: rect.radius * 2 + glowRadius: 2 + spread: 0.2 + color: Qt.rgba(0, 0, 0, 0.3) + } + Rectangle { + id: rect + anchors.fill: parent + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.Window + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.smallSpacing + + Kirigami.Separator { + id: topSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: dialog.header.implicitHeight + } + + Kirigami.Separator { + id: bottomSeparator + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: dialog.footer.implicitHeight + } + + Rectangle { + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + anchors.left: parent.left + anchors.right: parent.right + anchors.top: topSeparator.bottom + anchors.bottom: bottomSeparator.top + } + } + } +} + diff --git a/kcms/cellularnetwork/package/contents/ui/ProfileList.qml b/kcms/cellularnetwork/package/contents/ui/ProfileList.qml new file mode 100644 index 00000000..174aafab --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/ProfileList.qml @@ -0,0 +1,183 @@ +/* + SPDX-FileCopyrightText: 2020-2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls +import org.kde.kirigami 2.12 as Kirigami +import cellularnetworkkcm 1.0 + +Kirigami.ScrollablePage { + id: apnlist + title: i18n("APNs") + + property Modem modem + + ListView { + id: profileListView + model: modem.profiles + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Kirigami.Units.largeSpacing + visible: profileListView.count === 0 + text: i18n("No APNs configured") + icon.name: "globe" + + helpfulAction: Kirigami.Action { + iconName: "list-add" + text: i18n("Add Connection") + onTriggered: { + profileDialog.profile = null; + profileDialog.open(); + } + } + } + + EditProfileDialog { + id: profileDialog + modem: apnlist.modem + profile: null + pageWidth: apnlist.width + } + + header: ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + MessagesList { + id: messagesList + visible: count != 0 + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + model: kcm.messages + } + + Kirigami.InlineMessage { + id: cannotFindWarning + Layout.margins: visible ? Kirigami.Units.largeSpacing : 0 + Layout.topMargin: visible && !messagesList.visible ? Kirigami.Units.largeSpacing : 0 + Layout.fillWidth: true + + visible: false + type: Kirigami.MessageType.Warning + showCloseButton: true + text: qsTr("Unable to autodetect connection settings for your carrier. Please find your carrier's APN settings by either contacting support or searching online.") + + Connections { + target: modem + function onCouldNotAutodetectSettings() { + cannotFindWarning.visible = true; + } + } + } + + Kirigami.SwipeListItem { + Layout.fillWidth: true + visible: profileListView.count !== 0 + onClicked: { + profileDialog.profile = null; + profileDialog.open(); + } + + contentItem: Row { + spacing: Kirigami.Units.smallSpacing + Kirigami.Icon { + anchors.verticalCenter: parent.verticalCenter + source: "list-add" + height: Kirigami.Units.gridUnit * 1.5 + width: height + } + Kirigami.Heading { + level: 3 + anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignLeft + text: i18n("Add APN") + } + } + } + + Kirigami.SwipeListItem { + Layout.fillWidth: true + onClicked: { + modem.addDetectedProfileSettings(); + } + + contentItem: Row { + spacing: Kirigami.Units.smallSpacing + Kirigami.Icon { + anchors.verticalCenter: parent.verticalCenter + source: "list-add" + height: Kirigami.Units.gridUnit * 1.5 + width: height + } + Kirigami.Heading { + level: 3 + anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignLeft + text: i18n("Autodetect APN") + } + } + } + } + + delegate: Kirigami.SwipeListItem { + onClicked: modem.activateProfile(modelData.connectionUni) + + actions: [ + Kirigami.Action { + iconName: "entry-edit" + text: i18n("Edit") + onTriggered: { + profileDialog.profile = modelData; + profileDialog.open(); + } + }, + Kirigami.Action { + iconName: "delete" + text: i18n("Delete") + onTriggered: modem.removeProfile(modelData.connectionUni) + } + ] + + contentItem: RowLayout { + Layout.fillWidth: true + + Controls.RadioButton { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + checked: modem.activeConnectionUni == modelData.connectionUni + onClicked: { + if (!checked) { + modem.activateProfile(modelData.connectionUni); + } + + // reapply binding + checked = Qt.binding(() => { return modem.activeConnectionUni == modelData.connectionUni }); + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + Kirigami.Heading { + Layout.fillWidth: true + level: 3 + text: modelData.name + } + Controls.Label { + Layout.fillWidth: true + text: modelData.apn + } + } + } + } + } +} diff --git a/kcms/cellularnetwork/package/contents/ui/SimLockPage.qml b/kcms/cellularnetwork/package/contents/ui/SimLockPage.qml new file mode 100644 index 00000000..729bf9e1 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/SimLockPage.qml @@ -0,0 +1,249 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kcm 1.2 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import cellularnetworkkcm 1.0 + +Kirigami.ScrollablePage { + id: root + title: i18n("SIM Lock") + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + property Sim sim + + ColumnLayout { + spacing: 0 + width: root.width + + MessagesList { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + model: kcm.messages + } + + ColumnLayout { + id: unlockSimPlaceholder + visible: sim.locked + Layout.fillWidth: true + + // HACK: prevent infinite binding loop? + Component.onCompleted: unlockSimPlaceholder.Layout.preferredHeight = root.height + + Kirigami.PlaceholderMessage { + Layout.margins: Kirigami.Units.gridUnit * 2 + Layout.fillWidth: true + Layout.fillHeight: true + text: i18n("SIM is locked") + explanation: i18n("In order to use this SIM, you must first unlock it.") + icon.name: "lock" + helpfulAction: Kirigami.Action { + icon.name: "unlock" + text: "Unlock SIM" + onTriggered: unlockPinDialog.open() // TODO replace custom unlock pin dialog with just opening the system unlock PIN dialog + } + } + } + + ColumnLayout { + id: notLockedSimPlaceholder + visible: !sim.pinEnabled && !unlockSimPlaceholder.visible + Layout.fillWidth: true + + // HACK: prevent infinite binding loop? + Component.onCompleted: notLockedSimPlaceholder.Layout.preferredHeight = root.height + + Kirigami.PlaceholderMessage { + Layout.margins: Kirigami.Units.gridUnit * 2 + Layout.fillWidth: true + Layout.fillHeight: true + text: i18n("SIM is not locked") + explanation: i18n("You can lock your SIM to require a set PIN code for phone calls and mobile data.") + icon.name: "unlock" + helpfulAction: Kirigami.Action { + icon.name: "lock" + text: i18n("Lock SIM") + onTriggered: createPinDialog.open() + } + } + } + + MobileForm.FormCard { + visible: !notLockedSimPlaceholder.visible && !unlockSimPlaceholder.visible + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + MobileForm.FormButtonDelegate { + id: disableSimLockButton + text: i18n("Disable SIM Lock") + description: i18n("Disable the SIM lock feature and remove the passcode on the SIM.") + onClicked: removePinDialog.open(); + } + + Kirigami.Separator { + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + opacity: (!disableSimLockButton.controlHovered && !changePinButton.controlHovered) ? 0.5 : 0 + } + + MobileForm.FormButtonDelegate { + id: changePinButton + text: i18n("Change PIN") + description: i18n("Change the passcode set on the SIM.") + onClicked: changePinDialog.open() + } + } + } + + // dialogs + + RegExpValidator { + id: pinValidator + regExp: /[0-9]+/ + } + + Kirigami.Dialog { + id: unlockPinDialog + title: i18n("Unlock SIM") + standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel + padding: Kirigami.Units.largeSpacing + + onAccepted: sim.sendPin(unlockPinCurPin.text) + + ColumnLayout { + Controls.Label { + text: i18n("Attempts left: %1", sim.unlockRetriesLeft) + } + Kirigami.PasswordField { + id: unlockPinCurPin + placeholderText: i18n("Enter PIN") + validator: pinValidator + } + } + } + + Kirigami.Dialog { + id: changePinDialog + title: i18n("Change SIM PIN") + standardButtons: isValid ? Controls.Dialog.Ok | Controls.Dialog.Cancel : Controls.Dialog.Cancel + padding: Kirigami.Units.largeSpacing + + property bool isValid: changePinNewPin.text == changePinConfirmPin.text && + changePinNewPin.text.length >= 4 && changePinNewPin.text.length <= 8 // SIM PINs are between 4-8 digits + + onAccepted: { + if (isValid) { + sim.changePin(changePinCurPin.text, changePinNewPin.text); + } + } + + ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.InlineMessage { + id: changePinMatch + Layout.fillWidth: true + visible: changePinNewPin.text != changePinConfirmPin.text + text: i18n("PINs don't match!") + type: Kirigami.MessageType.Error + } + Kirigami.InlineMessage { + Layout.fillWidth: true + visible: !changePinMatch.visible && changePinNewPin.text.length != 0 && (changePinNewPin.text.length < 4 || changePinNewPin.text.length > 8) + text: i18n("PINs must be between 4 and 8 digits!") + type: Kirigami.MessageType.Error + } + Kirigami.PasswordField { + id: changePinCurPin + placeholderText: i18n("Current PIN") + validator: pinValidator + } + Kirigami.PasswordField { + id: changePinNewPin + placeholderText: i18n("New PIN") + validator: pinValidator + } + Kirigami.PasswordField { + id: changePinConfirmPin + placeholderText: i18n("Confirm PIN") + validator: pinValidator + } + } + } + + Kirigami.Dialog { + id: removePinDialog + title: i18n("Remove SIM PIN") + standardButtons: Controls.Dialog.Ok | Controls.Dialog.Cancel + padding: Kirigami.Units.largeSpacing + + onAccepted: sim.togglePinEnabled(removePinCurPin.text); + + ColumnLayout { + Kirigami.PasswordField { + id: removePinCurPin + placeholderText: i18n("Current PIN") + validator: pinValidator + } + } + } + + Kirigami.Dialog { + id: createPinDialog + title: i18n("Add SIM PIN") + standardButtons: isValid ? Controls.Dialog.Ok | Controls.Dialog.Cancel : Controls.Dialog.Cancel + padding: Kirigami.Units.largeSpacing + + property bool isValid: createPinNewPin.text == createPinConfirmPin.text && + createPinNewPin.text.length >= 4 && createPinNewPin.text.length <= 8 // SIM PINs are between 4-8 digits + + onAccepted: { + if (isValid) { + sim.togglePinEnabled(createPinNewPin.text); + } + } + + ColumnLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.InlineMessage { + id: createPinMatch + Layout.fillWidth: true + visible: createPinNewPin.text != createPinConfirmPin.text + text: i18n("PINs don't match!") + type: Kirigami.MessageType.Error + } + Kirigami.InlineMessage { + Layout.fillWidth: true + visible: !createPinMatch.visible && createPinNewPin.text.length != 0 && (createPinNewPin.text.length < 4 || createPinNewPin.text.length > 8) + text: i18n("PINs must be between 4 and 8 digits!") + type: Kirigami.MessageType.Error + } + Kirigami.PasswordField { + id: createPinNewPin + placeholderText: i18n("New PIN") + validator: pinValidator + } + Kirigami.PasswordField { + id: createPinConfirmPin + placeholderText: i18n("Confirm PIN") + validator: pinValidator + } + } + } + } +} + diff --git a/kcms/cellularnetwork/package/contents/ui/SimPage.qml b/kcms/cellularnetwork/package/contents/ui/SimPage.qml new file mode 100644 index 00000000..602b52e2 --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/SimPage.qml @@ -0,0 +1,230 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM +import org.kde.kcm 1.2 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import cellularnetworkkcm 1.0 + +Kirigami.ScrollablePage { + id: simPage + title: i18n("SIM") + " " + displayId + + property Sim sim: null + + property string displayId: sim ? sim.displayId : "" + property bool simEnabled: sim ? sim.enabled : false + property bool isRoaming: sim ? (sim.modem ? sim.modem.isRoaming : false) : false + + property bool simLocked: sim ? sim.locked : false + property string simImsi: sim ? sim.imsi : "" + property string simEid: sim ? sim.eid : "" + property string operatorCode: sim ? (sim.modem ? sim.modem.details.operatorCode : "") : "" + property string operatorName: sim ? (sim.modem ? sim.modem.details.operatorName : "") : "" + property string simOperatorIdentifier: sim ? sim.operatorIdentifier : "" + property string simOperatorName: sim ? sim.operatorName : "" + property string simIdentifier: sim ? sim.simIdentifier : "" + property var simEmergencyNumbers: sim ? sim.emergencyNumbers : [] + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + PlasmaNM.EnabledConnections { + id: enabledConnections + } + + ColumnLayout { + spacing: 0 + width: simPage.width + + Kirigami.InlineMessage { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + Layout.bottomMargin: visible && !messagesList.visible ? Kirigami.Units.largeSpacing : 0 + visible: !simEnabled + type: Kirigami.MessageType.Error + text: qsTr("This SIM slot is empty, a SIM card needs to be inserted in order for it to be used.") + } + + MessagesList { + id: messagesList + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + model: kcm.messages + } + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormSwitchDelegate { + id: dataRoamingCheckBox + text: i18n("Data Roaming") + description: i18n("Allow your device to use networks other than your carrier.") + enabled: simEnabled + checked: isRoaming + onCheckedChanged: sim.modem.isRoaming = checked + } + + MobileForm.FormDelegateSeparator { above: dataRoamingCheckBox; below: apnButton } + + MobileForm.FormButtonDelegate { + id: apnButton + icon.name: "globe" + text: i18n("Modify APNs") + description: i18n("Configure access point names for your carrier.") + enabled: simEnabled && enabledConnections.wwanEnabled + onClicked: kcm.push("ProfileList.qml", { "modem": sim.modem }); + } + + MobileForm.FormDelegateSeparator { above: apnButton; below: networksButton } + + MobileForm.FormButtonDelegate { + id: networksButton + icon.name: "network-mobile-available" + text: i18n("Networks") + description: i18n("Select a network operator.") + enabled: simEnabled + onClicked: kcm.push("AvailableNetworks.qml", { "modem": sim.modem, "sim": sim }); + } + + MobileForm.FormDelegateSeparator { above: networksButton; below: simLockButton } + + MobileForm.FormButtonDelegate { + id: simLockButton + icon.name: "unlock" + text: i18n("SIM Lock") + description: i18n("Modify SIM lock settings.") + enabled: simEnabled + onClicked: kcm.push("SimLockPage.qml", { "sim": sim }); + } + + MobileForm.FormDelegateSeparator { above: simLockButton; below: modemDetailsButton } + + MobileForm.FormButtonDelegate { + id: modemDetailsButton + icon.name: "network-modem" + text: i18n("Modem Details") + description: i18n("View the details of the modem this SIM is connected to.") + onClicked: kcm.push("ModemPage.qml", { "modem": sim.modem }) + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("SIM Details") + } + + MobileForm.FormTextDelegate { + id: lockedText + text: i18n("Locked") + description: simLocked ? i18n("Yes") : i18n("No") + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: imsiText + text: i18n("IMSI") + description: simImsi + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: eidText + text: i18n("EID") + description: simEid + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: opCodeModemText + text: i18n("Operator Code (modem)") + description: operatorCode + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: opNameModemText + text: i18n("Operator Name (modem)") + description: operatorName + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: opCodeSimText + text: i18n("Operator Code (provided by SIM)") + description: simOperatorIdentifier + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: opNameSimText + text: i18n("Operator Name (provided by SIM)") + description: simOperatorName + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: simIdText + text: i18n("SIM ID") + description: simIdentifier + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.AbstractFormDelegate { + id: emergencyNumbersText + Layout.fillWidth: true + + background: Item {} + contentItem: ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + Controls.Label { + Layout.fillWidth: true + text: i18n("Emergency Numbers") + elide: Text.ElideRight + } + + Repeater { + model: simEmergencyNumbers + + Controls.Label { + Layout.fillWidth: true + text: modelData + color: Kirigami.Theme.disabledTextColor + font: Kirigami.Theme.smallFont + elide: Text.ElideRight + } + } + } + } + } + } + } +} diff --git a/kcms/cellularnetwork/package/contents/ui/main.qml b/kcms/cellularnetwork/package/contents/ui/main.qml new file mode 100644 index 00000000..7549e6ed --- /dev/null +++ b/kcms/cellularnetwork/package/contents/ui/main.qml @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2018 Martin Kacej +// SPDX-FileCopyrightText: 2020 Dimitris Kardarakos +// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.12 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.12 as Controls + +import org.kde.plasma.networkmanagement 0.2 as PlasmaNM +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kcm 1.3 as KCM +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +import cellularnetworkkcm 1.0 + +KCM.SimpleKCM { + id: root + objectName: "mobileDataMain" + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + Kirigami.PlaceholderMessage { + id: noModem + anchors.centerIn: parent + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Kirigami.Units.largeSpacing + + visible: !enabledConnections.wwanHwEnabled || !availableDevices.modemDeviceAvailable || !kcm.modemFound + icon.name: "auth-sim-missing" + text: i18n("Modem not available") + } + + ColumnLayout { + spacing: 0 + width: root.width + visible: !noModem.visible + + PlasmaNM.Handler { + id: nmHandler + } + + PlasmaNM.AvailableDevices { + id: availableDevices + } + + PlasmaNM.EnabledConnections { + id: enabledConnections + } + + SimPage { + id: simPage + visible: false + } + + MessagesList { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + model: kcm.messages + } + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormSwitchDelegate { + id: mobileDataSwitch + text: i18n("Mobile data") + description: { + if (!kcm.modemFound) { + return ""; + } else if (!kcm.selectedModem.hasSim) { + return i18n("No SIM is inserted.") + } if (!kcm.selectedModem.mobileDataSupported) { + return i18n("Mobile data is not available with this modem.") + } else if (kcm.selectedModem.needsAPNAdded) { + return i18n("An APN needs to be configured to have mobile data."); + } else { + return i18n("Whether mobile data is enabled."); + } + } + + property bool manuallySet: false + property bool shouldBeChecked: kcm.selectedModem && kcm.selectedModem.mobileDataEnabled + onShouldBeCheckedChanged: { + checked = shouldBeChecked; + } + + enabled: kcm.selectedModem.mobileDataSupported && !kcm.selectedModem.needsAPNAdded + checked: shouldBeChecked + + onCheckedChanged: { + // prevent binding loops + if (manuallySet) { + manuallySet = false; + return; + } + + if (kcm.selectedModem.mobileDataEnabled != checked) { + manuallySet = true; + kcm.selectedModem.mobileDataEnabled = checked; + } + } + } + + MobileForm.FormDelegateSeparator { above: mobileDataSwitch; below: dataUsageButton } + + MobileForm.FormButtonDelegate { + id: dataUsageButton + text: i18n("Data Usage") + description: i18n("View data usage.") + icon.name: "office-chart-bar" + + enabled: false + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: kcm.sims.count == 1 ? i18n("SIM") : i18n("SIMs") + } + + Repeater { + id: repeater + model: kcm.sims + + delegate: ColumnLayout { + Layout.fillWidth: true + + Kirigami.Separator { + visible: model.index !== 0 + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + opacity: (!(model.index && repeater.itemAt(model.index - 1).controlHovered) && !simDelegate.controlHovered) ? 0.5 : 0 + } + + MobileForm.FormButtonDelegate { + id: simDelegate + text: i18n("SIM %1", modelData.displayId) + description: i18n("View SIM %1 details.", modelData.displayId) + icon.name: "auth-sim-symbolic" + onClicked: { + simPage.sim = modelData; + simPage.visible = true; + kcm.push(simPage); + } + } + } + } + } + } + } +} diff --git a/kcms/cellularnetwork/profilesettings.cpp b/kcms/cellularnetwork/profilesettings.cpp new file mode 100644 index 00000000..a5a02a8a --- /dev/null +++ b/kcms/cellularnetwork/profilesettings.cpp @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "profilesettings.h" + +#include + +ProfileSettings::ProfileSettings(QObject *parent, + QString name, + QString apn, + QString user, + QString password, + NetworkManager::GsmSetting::NetworkType networkType, + QString connectionUni) + : QObject{parent} + , m_name(name) + , m_apn(apn) + , m_user(user) + , m_password(password) + , m_networkType(networkTypeStr(networkType)) + , m_connectionUni(connectionUni) +{ + setParent(parent); +} + +ProfileSettings::ProfileSettings(QObject *parent, NetworkManager::Setting::Ptr setting, NetworkManager::Connection::Ptr connection) + : QObject{parent} + , m_connectionUni(connection->uuid()) +{ + setParent(parent); + + NetworkManager::GsmSetting::Ptr gsmSetting = setting.staticCast(); + + m_name = connection->name(); + m_apn = gsmSetting->apn(); + m_user = gsmSetting->username(); + m_password = gsmSetting->password(); + m_networkType = networkTypeStr(gsmSetting->networkType()); +} + +QString ProfileSettings::name() +{ + return m_name; +} + +QString ProfileSettings::apn() +{ + return m_apn; +} + +void ProfileSettings::setApn(QString apn) +{ + if (apn != m_apn) { + m_apn = apn; + Q_EMIT apnChanged(); + } +} + +QString ProfileSettings::user() +{ + return m_user; +} + +void ProfileSettings::setUser(QString user) +{ + if (user != m_user) { + m_user = user; + Q_EMIT userChanged(); + } +} + +QString ProfileSettings::password() +{ + return m_password; +} + +void ProfileSettings::setPassword(QString password) +{ + if (password != m_password) { + m_password = password; + Q_EMIT passwordChanged(); + } +} + +QString ProfileSettings::networkType() +{ + return m_networkType; +} + +void ProfileSettings::setNetworkType(QString networkType) +{ + if (networkType != m_networkType) { + m_networkType = networkType; + Q_EMIT networkTypeChanged(); + } +} + +QString ProfileSettings::connectionUni() +{ + return m_connectionUni; +} + +QString ProfileSettings::networkTypeStr(NetworkManager::GsmSetting::NetworkType networkType) +{ + if (networkType == NetworkManager::GsmSetting::NetworkType::Any) { + return QStringLiteral("Any"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::GprsEdgeOnly) { + return QStringLiteral("Only 2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Only3G) { + return QStringLiteral("Only 3G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Only4GLte) { + return QStringLiteral("Only 4G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer2G) { + return QStringLiteral("2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer3G) { + return QStringLiteral("3G/2G"); + } else if (networkType == NetworkManager::GsmSetting::NetworkType::Prefer4GLte) { + return QStringLiteral("4G/3G/2G"); + } + return QStringLiteral("Any"); +} + +NetworkManager::GsmSetting::NetworkType ProfileSettings::networkTypeFlag(const QString &networkType) +{ + if (networkType == QStringLiteral("Any")) { + return NetworkManager::GsmSetting::NetworkType::Any; + } else if (networkType == QStringLiteral("Only 2G")) { + return NetworkManager::GsmSetting::NetworkType::GprsEdgeOnly; + } else if (networkType == QStringLiteral("Only 3G")) { + return NetworkManager::GsmSetting::NetworkType::Only3G; + } else if (networkType == QStringLiteral("Only 4G")) { + return NetworkManager::GsmSetting::NetworkType::Only4GLte; + } else if (networkType == QStringLiteral("2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer2G; + } else if (networkType == QStringLiteral("3G/2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer3G; + } else if (networkType == QStringLiteral("4G/3G/2G")) { + return NetworkManager::GsmSetting::NetworkType::Prefer4GLte; + } + return NetworkManager::GsmSetting::NetworkType::Any; +} diff --git a/kcms/cellularnetwork/profilesettings.h b/kcms/cellularnetwork/profilesettings.h new file mode 100644 index 00000000..cd117398 --- /dev/null +++ b/kcms/cellularnetwork/profilesettings.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2021-2022 Devin Lin +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "cellularnetworksettings.h" +#include "modemdetails.h" +#include "sim.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class ProfileSettings : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString apn READ apn WRITE setApn NOTIFY apnChanged) + Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged) + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString networkType READ networkType WRITE setNetworkType NOTIFY networkTypeChanged) + Q_PROPERTY(QString connectionUni READ connectionUni NOTIFY connectionUniChanged) + +public: + ProfileSettings(QObject *parent = nullptr) + : QObject{parent} + { + } + ProfileSettings(QObject *parent, + QString name, + QString apn, + QString user, + QString password, + NetworkManager::GsmSetting::NetworkType networkType, + QString connectionUni); + ProfileSettings(QObject *parent, NetworkManager::Setting::Ptr settings, NetworkManager::Connection::Ptr connection); + + QString name(); + QString apn(); + void setApn(QString apn); + QString user(); + void setUser(QString user); + QString password(); + void setPassword(QString password); + QString networkType(); + void setNetworkType(QString ipType); + QString connectionUni(); + + // utilities + static QString networkTypeStr(NetworkManager::GsmSetting::NetworkType networkType); + static NetworkManager::GsmSetting::NetworkType networkTypeFlag(const QString &networkType); + +Q_SIGNALS: + void nameChanged(); + void apnChanged(); + void userChanged(); + void passwordChanged(); + void networkTypeChanged(); + void connectionUniChanged(); + +private: + QString m_name; + QString m_apn; + QString m_user; + QString m_password; + QString m_networkType; + QString m_connectionUni; +}; diff --git a/kcms/cellularnetwork/sim.cpp b/kcms/cellularnetwork/sim.cpp new file mode 100644 index 00000000..a24d3ab4 --- /dev/null +++ b/kcms/cellularnetwork/sim.cpp @@ -0,0 +1,194 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#include "sim.h" + +#include + +Sim::Sim(QObject *parent, Modem *modem, ModemManager::Sim::Ptr mmSim, ModemManager::Modem::Ptr mmModem, ModemManager::Modem3gpp::Ptr mmModem3gpp) + : QObject{parent} + , m_modem{modem} + , m_mmSim{mmSim} + , m_mmModem{mmModem} + , m_mmModem3gpp{mmModem3gpp} +{ + connect(m_mmSim.data(), &ModemManager::Sim::imsiChanged, this, [this]() -> void { + Q_EMIT imsiChanged(); + }); + connect(m_mmSim.data(), &ModemManager::Sim::operatorIdentifierChanged, this, [this]() -> void { + Q_EMIT operatorIdentifierChanged(); + }); + connect(m_mmSim.data(), &ModemManager::Sim::operatorNameChanged, this, [this]() -> void { + Q_EMIT operatorNameChanged(); + }); + connect(m_mmSim.data(), &ModemManager::Sim::simIdentifierChanged, this, [this]() -> void { + Q_EMIT simIdentifierChanged(); + }); + + connect(m_mmModem.data(), &ModemManager::Modem::unlockRequiredChanged, this, [this]() -> void { + Q_EMIT lockedChanged(); + Q_EMIT lockedReasonChanged(); + }); + + if (m_mmModem3gpp) { + connect(m_mmModem3gpp.data(), &ModemManager::Modem3gpp::enabledFacilityLocksChanged, this, [this]() -> void { + Q_EMIT pinEnabledChanged(); + }); + } +} + +bool Sim::enabled() +{ + return uni() != QStringLiteral("/"); +} + +bool Sim::pinEnabled() +{ + return m_mmModem3gpp && (m_mmModem3gpp->enabledFacilityLocks() & MM_MODEM_3GPP_FACILITY_SIM); +} + +int Sim::unlockRetriesLeft() +{ + return m_mmModem->unlockRetries()[MM_MODEM_LOCK_SIM_PIN]; +} + +bool Sim::locked() +{ + return m_mmModem->unlockRequired() == MM_MODEM_LOCK_SIM_PIN; +} + +QString Sim::lockedReason() +{ + switch (m_mmModem->unlockRequired()) { + case MM_MODEM_LOCK_UNKNOWN: + return i18n("Lock reason unknown."); + case MM_MODEM_LOCK_NONE: + return i18n("Modem is unlocked."); + case MM_MODEM_LOCK_SIM_PIN: + return i18n("SIM requires the PIN code."); + case MM_MODEM_LOCK_SIM_PIN2: + return i18n("SIM requires the PIN2 code."); + case MM_MODEM_LOCK_SIM_PUK: + return i18n("SIM requires the PUK code."); + case MM_MODEM_LOCK_SIM_PUK2: + return i18n("SIM requires the PUK2 code."); + case MM_MODEM_LOCK_PH_SP_PIN: + return i18n("Modem requires the service provider PIN code."); + case MM_MODEM_LOCK_PH_SP_PUK: + return i18n("Modem requires the service provider PUK code."); + case MM_MODEM_LOCK_PH_NET_PIN: + return i18n("Modem requires the network PIN code."); + case MM_MODEM_LOCK_PH_NET_PUK: + return i18n("Modem requires the network PUK code."); + case MM_MODEM_LOCK_PH_SIM_PIN: + return i18n("Modem requires the PIN code."); + case MM_MODEM_LOCK_PH_CORP_PIN: + return i18n("Modem requires the corporate PIN code."); + case MM_MODEM_LOCK_PH_CORP_PUK: + return i18n("Modem requires the corporate PUK code."); + case MM_MODEM_LOCK_PH_FSIM_PIN: + return i18n("Modem requires the PH-FSIM PIN code."); + case MM_MODEM_LOCK_PH_FSIM_PUK: + return i18n("Modem requires the PH-FSIM PUK code."); + case MM_MODEM_LOCK_PH_NETSUB_PIN: + return i18n("Modem requires the network subset PIN code."); + case MM_MODEM_LOCK_PH_NETSUB_PUK: + return i18n("Modem requires the network subset PUK code."); + } + return QString{}; +} + +QString Sim::imsi() +{ + return m_mmSim->imsi(); +} + +QString Sim::eid() +{ + return ""; // TODO add in mm-qt +} + +QString Sim::operatorIdentifier() +{ + return m_mmSim->operatorIdentifier(); +} + +QString Sim::operatorName() +{ + return m_mmSim->operatorName(); +} + +QString Sim::simIdentifier() +{ + return m_mmSim->simIdentifier(); +} + +QStringList Sim::emergencyNumbers() +{ + return {}; // TODO add in mm-qt +} + +QString Sim::uni() +{ + return m_mmSim->uni(); +} + +QString Sim::displayId() +{ + // in the form /org/freedesktop/ModemManager1/Sim/0 + QStringList uniSplit = uni().split(QStringLiteral("/")); + return (uniSplit.count() == 0 || uni() == "/") ? i18n("(empty)") : QString(uniSplit[uniSplit.size() - 1]); +} + +Modem *Sim::modem() +{ + return m_modem; +} + +void Sim::togglePinEnabled(const QString &pin) +{ + bool isPinEnabled = pinEnabled(); + QDBusPendingReply reply = m_mmSim->enablePin(pin, !isPinEnabled); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error toggling SIM lock to") << isPinEnabled << QStringLiteral(":") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error toggling SIM lock: %1", reply.error().message())); + } +} + +void Sim::changePin(const QString &oldPin, const QString &newPin) +{ + QDBusPendingReply reply = m_mmSim->changePin(oldPin, newPin); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error changing the PIN:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error changing the PIN: %1", reply.error().message())); + } +} + +void Sim::sendPin(const QString &pin) +{ + if (m_mmModem->unlockRequired() != MM_MODEM_LOCK_NONE) { + QDBusPendingReply reply = m_mmSim->sendPin(pin); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error sending the PIN:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error sending the PIN: %1", reply.error().message())); + } + } +} + +void Sim::sendPuk(const QString &pin, const QString &puk) +{ + if (m_mmModem->unlockRequired() != MM_MODEM_LOCK_NONE) { + QDBusPendingReply reply = m_mmSim->sendPuk(pin, puk); + reply.waitForFinished(); + if (reply.isError()) { + qWarning() << QStringLiteral("Error sending the PUK:") << reply.error().message(); + CellularNetworkSettings::instance()->addMessage(InlineMessage::Error, i18n("Error sending the PUK: %1", reply.error().message())); + } + } +} diff --git a/kcms/cellularnetwork/sim.h b/kcms/cellularnetwork/sim.h new file mode 100644 index 00000000..73541e75 --- /dev/null +++ b/kcms/cellularnetwork/sim.h @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: GPL-3.0-or-later +*/ + +#pragma once + +#include "modem.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class Modem; + +class Sim : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged) + Q_PROPERTY(bool pinEnabled READ pinEnabled NOTIFY pinEnabledChanged) // if there is a PIN set on the SIM + Q_PROPERTY(int unlockRetriesLeft READ unlockRetriesLeft NOTIFY unlockRetriesLeftChanged) + Q_PROPERTY(bool locked READ locked NOTIFY lockedChanged) // if the SIM is currently locked (requires entering PIN) + Q_PROPERTY(QString lockedReason READ lockedReason NOTIFY lockedReasonChanged) + Q_PROPERTY(QString imsi READ imsi NOTIFY imsiChanged) + Q_PROPERTY(QString eid READ eid NOTIFY eidChanged) // TODO (not in mm-qt) + Q_PROPERTY(QString operatorIdentifier READ operatorIdentifier NOTIFY operatorIdentifierChanged) + Q_PROPERTY(QString operatorName READ operatorName NOTIFY operatorNameChanged) + Q_PROPERTY(QString simIdentifier READ simIdentifier NOTIFY simIdentifierChanged) + Q_PROPERTY(QStringList emergencyNumbers READ emergencyNumbers NOTIFY emergencyNumbersChanged) + Q_PROPERTY(QString uni READ uni NOTIFY uniChanged) + Q_PROPERTY(QString displayId READ displayId NOTIFY displayIdChanged) + Q_PROPERTY(Modem *modem READ modem NOTIFY modemChanged) + +public: + Sim(QObject *parent = nullptr, + Modem *modem = nullptr, + ModemManager::Sim::Ptr mmSim = ModemManager::Sim::Ptr{nullptr}, + ModemManager::Modem::Ptr mmModem = ModemManager::Modem::Ptr{nullptr}, + ModemManager::Modem3gpp::Ptr mmModem3gpp = ModemManager::Modem3gpp::Ptr{nullptr}); + + bool enabled(); + bool pinEnabled(); + int unlockRetriesLeft(); + bool locked(); + QString lockedReason(); + QString imsi(); + QString eid(); // TODO add in mm-qt + QString operatorIdentifier(); + QString operatorName(); + QString simIdentifier(); + QStringList emergencyNumbers(); // TODO add in mm-qt + QString uni(); + QString displayId(); + Modem *modem(); + + Q_INVOKABLE void togglePinEnabled(const QString &pin); + Q_INVOKABLE void changePin(const QString &oldPin, const QString &newPin); + Q_INVOKABLE void sendPin(const QString &pin); + Q_INVOKABLE void sendPuk(const QString &pin, const QString &puk); + +Q_SIGNALS: + void enabledChanged(); + void pinEnabledChanged(); + void unlockRetriesLeftChanged(); + void lockedChanged(); + void lockedReasonChanged(); + void imsiChanged(); + void eidChanged(); + void operatorIdentifierChanged(); + void operatorNameChanged(); + void simIdentifierChanged(); + void emergencyNumbersChanged(); + void uniChanged(); + void displayIdChanged(); + void modemChanged(); + +private: + Modem *m_modem; + ModemManager::Sim::Ptr m_mmSim; + ModemManager::Modem::Ptr m_mmModem; + ModemManager::Modem3gpp::Ptr m_mmModem3gpp; // this may be a nullptr if no sim is inserted +}; diff --git a/kcms/info/CMakeLists.txt b/kcms/info/CMakeLists.txt new file mode 100644 index 00000000..8fd1405e --- /dev/null +++ b/kcms/info/CMakeLists.txt @@ -0,0 +1,22 @@ +set(info_SRCS # Specify source files for the library + info.cpp + distroinfo.cpp + softwareinfo.cpp + hardwareinfo.cpp +) + +add_library(kcm_mobile_info MODULE ${info_SRCS}) + +target_link_libraries(kcm_mobile_info + Qt::Core + KF6::CoreAddons + KF6::I18n + KF6::QuickAddons + KF6::ConfigCore + KF6::Solid +) + +install(TARGETS kcm_mobile_info DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) # Install the library to the kcm location + +kpackage_install_package(package kcm_mobile_info kcms) # Finally install our QML kpackage. + diff --git a/kcms/info/Messages.sh b/kcms/info/Messages.sh new file mode 100644 index 00000000..9f59426c --- /dev/null +++ b/kcms/info/Messages.sh @@ -0,0 +1 @@ +$XGETTEXT $(find . -name \*.cpp -o -name \*.h -o -name \*.qml) -o $podir/kcm_mobile_info.pot diff --git a/kcms/info/distroinfo.cpp b/kcms/info/distroinfo.cpp new file mode 100644 index 00000000..c02d45da --- /dev/null +++ b/kcms/info/distroinfo.cpp @@ -0,0 +1,12 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "distroinfo.h" + +DistroInfo::DistroInfo(QObject *parent) + : QObject(parent) +{ +} diff --git a/kcms/info/distroinfo.h b/kcms/info/distroinfo.h new file mode 100644 index 00000000..4f035478 --- /dev/null +++ b/kcms/info/distroinfo.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include +#include + +// clang-format off + +#define PROPERTY(type, name) \ + type name() const { return m_osrelease.name(); }\ + +// clang-format off + +#ifndef DISTROINFO_H +#define DISTROINFO_H + +class DistroInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QStringList idLike READ idLike CONSTANT) + Q_PROPERTY(QString versionCodename READ versionCodename CONSTANT) + Q_PROPERTY(QString versionId READ versionId CONSTANT) + Q_PROPERTY(QString prettyName READ prettyName CONSTANT) + Q_PROPERTY(QString ansiColor READ ansiColor CONSTANT) + Q_PROPERTY(QString cpeName READ cpeName CONSTANT) + Q_PROPERTY(QString homeUrl READ homeUrl CONSTANT) + Q_PROPERTY(QString documentationUrl READ documentationUrl CONSTANT) + Q_PROPERTY(QString supportUrl READ supportUrl CONSTANT) + Q_PROPERTY(QString bugReportUrl READ bugReportUrl CONSTANT) + Q_PROPERTY(QString privacyPolicyUrl READ privacyPolicyUrl CONSTANT) + Q_PROPERTY(QString buildId READ buildId CONSTANT) + Q_PROPERTY(QString variant READ variant CONSTANT) + Q_PROPERTY(QString variantId READ variantId CONSTANT) + Q_PROPERTY(QString logo READ logo CONSTANT) + +public: + DistroInfo(QObject *parent = nullptr); + + PROPERTY(QString, name) + PROPERTY(QString, version) + PROPERTY(QString, id) + PROPERTY(QStringList, idLike) + PROPERTY(QString, versionCodename) + PROPERTY(QString, versionId) + PROPERTY(QString, prettyName) + PROPERTY(QString, ansiColor) + PROPERTY(QString, cpeName) + PROPERTY(QString, homeUrl) + PROPERTY(QString, documentationUrl) + PROPERTY(QString, supportUrl) + PROPERTY(QString, bugReportUrl) + PROPERTY(QString, privacyPolicyUrl) + PROPERTY(QString, buildId) + PROPERTY(QString, variant) + PROPERTY(QString, variantId) + PROPERTY(QString, logo) + +private: + KOSRelease m_osrelease; +}; + +#endif // DISTROINFO_H diff --git a/kcms/info/hardwareinfo.cpp b/kcms/info/hardwareinfo.cpp new file mode 100644 index 00000000..4bf163ee --- /dev/null +++ b/kcms/info/hardwareinfo.cpp @@ -0,0 +1,89 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + SPDX-FileCopyrightText: 2012-2019 Harald Sitter + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "hardwareinfo.h" + +#include +#include + +#include +#include + +#include + +#ifdef Q_OS_LINUX +#include +#elif defined(Q_OS_FREEBSD) +#include +#include +#endif + +HardwareInfo::HardwareInfo(QObject *parent) + : QObject(parent) +{ +} + +int HardwareInfo::processorCount() const +{ + return Solid::Device::listFromType(Solid::DeviceInterface::Processor).count(); +} + +QString HardwareInfo::processors() const +{ + const auto list = Solid::Device::listFromType(Solid::DeviceInterface::Processor); + + // Format processor string + // Group by processor name + QMap processorMap; + for (const auto &device : list) { + const QString name = device.product(); + auto it = processorMap.find(name); + if (it == processorMap.end()) { + processorMap.insert(name, 1); + } else { + ++it.value(); + } + } + // Create a formatted list of grouped processors + QStringList names; + names.reserve(processorMap.count()); + for (auto it = processorMap.constBegin(); it != processorMap.constEnd(); ++it) { + const int count = it.value(); + QString name = it.key(); + name.replace(QStringLiteral("(TM)"), QChar(8482)); + name.replace(QStringLiteral("(R)"), QChar(174)); + name = name.simplified(); + names.append(QStringLiteral("%1 × %2").arg(count).arg(name)); + } + + const QString processorLabel = names.join(QLatin1String(", ")); + + return processorLabel; +} + +QString HardwareInfo::memory() const +{ + qlonglong totalRam = -1; +#ifdef Q_OS_LINUX + struct sysinfo info { + }; + if (sysinfo(&info) == 0) + // manpage "sizes are given as multiples of mem_unit bytes" + totalRam = qlonglong(info.totalram) * info.mem_unit; +#elif defined(Q_OS_FREEBSD) + /* Stuff for sysctl */ + size_t len; + + unsigned long memory; + len = sizeof(memory); + sysctlbyname("hw.physmem", &memory, &len, NULL, 0); + + totalRam = memory; +#endif + + return KFormat().formatByteSize(totalRam); +} diff --git a/kcms/info/hardwareinfo.h b/kcms/info/hardwareinfo.h new file mode 100644 index 00000000..0e4bcf7b --- /dev/null +++ b/kcms/info/hardwareinfo.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include + +#ifndef HARDWAREINFO_H +#define HARDWAREINFO_H + +class HardwareInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString processors READ processors CONSTANT) + Q_PROPERTY(int processorCount READ processorCount CONSTANT) + Q_PROPERTY(QString memory READ memory CONSTANT) + +public: + HardwareInfo(QObject *parent = nullptr); + + QString processors() const; + int processorCount() const; + QString memory() const; +}; + +#endif // HARDWAREINFO_H diff --git a/kcms/info/info.cpp b/kcms/info/info.cpp new file mode 100644 index 00000000..98b844e0 --- /dev/null +++ b/kcms/info/info.cpp @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "info.h" + +#include +#include +#include +#include + +K_PLUGIN_CLASS_WITH_JSON(Info, "info.json") + +Info::Info(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, metaData, args) + , m_distroInfo(new DistroInfo(this)) + , m_softwareInfo(new SoftwareInfo(this)) + , m_hardwareInfo(new HardwareInfo(this)) +{ + setButtons(Apply | Default); + + qDebug() << "Info module loaded."; +} + +void Info::copyInfoToClipboard() const +{ + const QString clipboardText = QStringLiteral( + "Operating System: %1\n" + "KDE Plasma Version: %2\n" + "KDE Frameworks Version: %3\n" + "Qt Version: %4\n" + "Kernel Version: %5\n" + "OS-Type: %6\n" + "Processor: %7\n" + "Memory: %8\n") + .arg(distroInfo()->name(), + softwareInfo()->plasmaVersion(), + softwareInfo()->frameworksVersion(), + softwareInfo()->qtVersion(), + softwareInfo()->kernelRelease(), + softwareInfo()->osType(), + hardwareInfo()->processors(), + hardwareInfo()->memory()); + + QGuiApplication::clipboard()->setText(clipboardText); +} + +DistroInfo *Info::distroInfo() const +{ + return m_distroInfo; +} + +SoftwareInfo *Info::softwareInfo() const +{ + return m_softwareInfo; +} + +HardwareInfo *Info::hardwareInfo() const +{ + return m_hardwareInfo; +} + +#include "info.moc" diff --git a/kcms/info/info.h b/kcms/info/info.h new file mode 100644 index 00000000..51a8be75 --- /dev/null +++ b/kcms/info/info.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "distroinfo.h" +#include "hardwareinfo.h" +#include "softwareinfo.h" +#include + +#ifndef INFO_H +#define INFO_H + +class Info : public KQuickAddons::ConfigModule +{ + Q_OBJECT + + Q_PROPERTY(DistroInfo *distroInfo READ distroInfo NOTIFY distroInfoChanged) + Q_PROPERTY(SoftwareInfo *softwareInfo READ softwareInfo NOTIFY softwareInfoChanged) + Q_PROPERTY(HardwareInfo *hardwareInfo READ hardwareInfo NOTIFY hardwareInfoChanged) + DistroInfo *distroInfo() const; + SoftwareInfo *softwareInfo() const; + HardwareInfo *hardwareInfo() const; + +public: + Info(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args); + + Q_INVOKABLE void copyInfoToClipboard() const; + +Q_SIGNALS: + void distroInfoChanged(); + void softwareInfoChanged(); + void hardwareInfoChanged(); + +private: + DistroInfo *m_distroInfo; + SoftwareInfo *m_softwareInfo; + HardwareInfo *m_hardwareInfo; +}; + +#endif // INFO_H diff --git a/kcms/info/info.json b/kcms/info/info.json new file mode 100644 index 00000000..a8372ecd --- /dev/null +++ b/kcms/info/info.json @@ -0,0 +1,123 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-system;", + "KPlugin": { + "Description": "Software Versions", + "Description[az]": "Proqram təminatı versiyası", + "Description[ca@valencia]": "Versions de programari", + "Description[ca]": "Versions de programari", + "Description[cs]": "Verze softwaru", + "Description[de]": "Software-Versionen", + "Description[en_GB]": "Software Versions", + "Description[es]": "Versiones de software", + "Description[eu]": "Softwarearen bertsioak", + "Description[fi]": "Ohjelmaversiot", + "Description[fr]": "Versions des logiciels", + "Description[hu]": "Szoftververziók", + "Description[ia]": "Versiones de software", + "Description[id]": "Versi Perangkat Lunak", + "Description[is]": "Útgáfur hugbúnaðar", + "Description[it]": "Versioni software", + "Description[ka]": "პროგრამების ვერსიები", + "Description[ko]": "소프트웨어 버전", + "Description[lt]": "Programinės įrangos versijos", + "Description[nl]": "Softwareversies", + "Description[nn]": "Programversjonar", + "Description[pa]": "ਸਾਫਟਵੇਅਰ ਵਰਜ਼ਨ", + "Description[pl]": "Wersje oprogramowania", + "Description[pt]": "Versões do 'Software'", + "Description[pt_BR]": "Versões do software", + "Description[ro]": "Versiuni de programe", + "Description[ru]": "Версии приложений", + "Description[sl]": "Verzije programja", + "Description[sv]": "Programvaruversioner", + "Description[tr]": "Yazılım Sürümleri", + "Description[uk]": "Версії програмного забезпечення", + "Description[vi]": "Các phiên bản phần mềm", + "Description[x-test]": "xxSoftware Versionsxx", + "Description[zh_CN]": "软件版本", + "Description[zh_TW]": "軟體版本", + "FormFactors": [ + "handset", + "tablet", + "mediacenter" + ], + "Icon": "dialog-information", + "Name": "Information", + "Name[az]": "Məlumat", + "Name[ca@valencia]": "Informació", + "Name[ca]": "Informació", + "Name[cs]": "Informace", + "Name[da]": "Information", + "Name[de]": "Informationen", + "Name[en_GB]": "Information", + "Name[es]": "Información", + "Name[et]": "Teave", + "Name[eu]": "Informazioa", + "Name[fi]": "Tiedot", + "Name[fr]": "Informations", + "Name[gl]": "Información", + "Name[hu]": "Információk", + "Name[ia]": "Information", + "Name[id]": "Informasi", + "Name[is]": "Upplýsingar", + "Name[it]": "Informazioni", + "Name[ka]": "ინფორმაცია", + "Name[ko]": "정보", + "Name[lt]": "Informacija", + "Name[nl]": "Informatie", + "Name[nn]": "Informasjon", + "Name[pa]": "ਜਾਣਕਾਰੀ", + "Name[pl]": "Informacje", + "Name[pt]": "Informação", + "Name[pt_BR]": "Informações", + "Name[ro]": "Informații", + "Name[ru]": "Сведения", + "Name[sk]": "Informácie", + "Name[sl]": "Informacije", + "Name[sv]": "Information", + "Name[ta]": "விவரங்கள்", + "Name[tr]": "Bilgi", + "Name[uk]": "Інформація", + "Name[vi]": "Thông tin", + "Name[x-test]": "xxInformationxx", + "Name[zh_CN]": "信息", + "Name[zh_TW]": "資訊" + }, + "X-KDE-Keywords": "info, distro, system", + "X-KDE-Keywords[ast]": "info, información, systema", + "X-KDE-Keywords[az]": "info, distro, system,məlumat,distribütor,sistem", + "X-KDE-Keywords[ca@valencia]": "informació, distribució, sistema", + "X-KDE-Keywords[ca]": "informació, distribució, sistema", + "X-KDE-Keywords[cs]": "informace, distribuce, systém", + "X-KDE-Keywords[da]": "info, distribution, system", + "X-KDE-Keywords[de]": "information,distribution,system", + "X-KDE-Keywords[en_GB]": "info, distro, system", + "X-KDE-Keywords[es]": "información, distro, sistema", + "X-KDE-Keywords[et]": "teave, info, süsteem", + "X-KDE-Keywords[eu]": "informazioa, banaketa, sistema", + "X-KDE-Keywords[fi]": "tietoa, jakelu, järjestelmä", + "X-KDE-Keywords[fr]": "info, distrib, système", + "X-KDE-Keywords[gl]": "info, información, distro, distribución, sistema", + "X-KDE-Keywords[hu]": "információ, disztró, rendszer", + "X-KDE-Keywords[ia]": "info, distro, system", + "X-KDE-Keywords[it]": "informazioni, distribuzione, sistema", + "X-KDE-Keywords[ko]": "info, distro, system, 정보, 배포판, 시스템", + "X-KDE-Keywords[lt]": "informacija, platinamasis paketas, platinimas, distribucija, sistema, distributyvas", + "X-KDE-Keywords[nl]": "informatie, distributie, systeem", + "X-KDE-Keywords[nn]": "info, distro, distribusjon, system", + "X-KDE-Keywords[pa]": "ਜਾਣਕਾਰੀ, ਡਿਸਟਰੋ, ਸਿਸਟਮ", + "X-KDE-Keywords[pl]": "info, distro, system", + "X-KDE-Keywords[pt]": "informação, distribuição, sistema", + "X-KDE-Keywords[pt_BR]": "informação, info, distro, distribuição, sistema", + "X-KDE-Keywords[ro]": "informații, info, distribuție, sistem", + "X-KDE-Keywords[ru]": "info,distro,system,информация,дистрибутив,сведения,система", + "X-KDE-Keywords[sk]": "info, distro, systém", + "X-KDE-Keywords[sl]": "info, distro, system,sistem", + "X-KDE-Keywords[sv]": "information, distribution, system", + "X-KDE-Keywords[tr]": "bilgi, dağıtım, sistem", + "X-KDE-Keywords[uk]": "info, distro, system, інфо, відомості, дистрибутив, система", + "X-KDE-Keywords[vi]": "info,distro,system,thông tin,bản phân phối,hệ thống", + "X-KDE-Keywords[x-test]": "xxinfoxx,xx distroxx,xx systemxx", + "X-KDE-Keywords[zh_CN]": "info, distro, system, 信息, 发行版, 系统", + "X-KDE-Keywords[zh_TW]": "info, distro, system, 資訊, 發行版, 系統, 信息" +} diff --git a/kcms/info/package/contents/ui/main.qml b/kcms/info/package/contents/ui/main.qml new file mode 100644 index 00000000..9ab63884 --- /dev/null +++ b/kcms/info/package/contents/ui/main.qml @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick.Layouts 1.2 +import QtQuick 2.7 +import QtQuick.Controls 2.2 as Controls + +import org.kde.kcm 1.2 as KCM +import org.kde.kirigami 2.10 as Kirigami +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +KCM.SimpleKCM { + title: i18n("System Information") + + leftPadding: 0 + rightPadding: 0 + Kirigami.Theme.colorSet: Kirigami.Theme.Window + + ColumnLayout { + width: parent.width + spacing: 0 + + Kirigami.Icon { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.gridUnit + Layout.bottomMargin: Kirigami.Units.gridUnit + implicitWidth: Kirigami.Units.iconSizes.huge + implicitHeight: width + source: kcm.distroInfo.logo ? kcm.distroInfo.logo : "kde" + } + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormTextDelegate { + text: i18n("Operating System") + description: kcm.distroInfo.name + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormButtonDelegate { + text: i18n("Webpage") + description: kcm.distroInfo.homeUrl + onClicked: { + Qt.openUrlExternally(kcm.distroInfo.homeUrl) + } + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: "Software" + } + + MobileForm.FormTextDelegate { + text: i18n("KDE Plasma Version") + description: kcm.softwareInfo.plasmaVersion + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + text: i18n("KDE Frameworks Version") + description: kcm.softwareInfo.frameworksVersion + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + text: i18n("Qt Version") + description: kcm.softwareInfo.qtVersion + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + text: i18n("Kernel Version") + description: kcm.softwareInfo.kernelRelease + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + text: i18n("OS Type") + description: i18nc("@label %1 is the CPU bit width (e.g. 32 or 64)", "%1-bit", kcm.softwareInfo.osType) + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: "Hardware" + } + + MobileForm.FormTextDelegate { + text: i18np("Processor", "Processors", kcm.hardwareInfo.processorCount); + description: kcm.hardwareInfo.processors + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + text: i18n("Memory") + description: { + if (kcm.hardwareInfo.memory !== "0 B") { + return i18nc("@label %1 is the formatted amount of system memory (e.g. 7,7 GiB)", + "%1 of RAM", kcm.hardwareInfo.memory) + } else { + return i18nc("Unknown amount of RAM", "Unknown") + } + } + } + } + } + } + + footer: RowLayout { + Item { + Layout.fillWidth: true + } + + Controls.Button { + text: i18n("Copy to clipboard") + icon.name: "edit-copy" + onClicked: kcm.copyInfoToClipboard() + } + } +} diff --git a/kcms/info/softwareinfo.cpp b/kcms/info/softwareinfo.cpp new file mode 100644 index 00000000..a316be87 --- /dev/null +++ b/kcms/info/softwareinfo.cpp @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + SPDX-FileCopyrightText: 2012-2019 Harald Sitter + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "softwareinfo.h" +#include + +#include +#include +#include +#include +#include +#include + +SoftwareInfo::SoftwareInfo(QObject *parent) + : QObject(parent) +{ +} + +QString SoftwareInfo::kernelRelease() const +{ + struct utsname utsName { + }; + uname(&utsName); + + return QString::fromLatin1(utsName.release); +} + +QString SoftwareInfo::frameworksVersion() const +{ + return KCoreAddons::versionString(); +} + +QString SoftwareInfo::qtVersion() const +{ + return QString::fromLatin1(qVersion()); +} + +QString SoftwareInfo::plasmaVersion() const +{ + const QStringList &filePaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wayland-sessions/plasma.desktop")); + + if (filePaths.length() < 1) { + return QString(); + } + + // Despite the fact that there can be multiple desktop files we simply take + // the first one as users usually don't have xsessions/ in their $HOME + // data location, so the first match should (usually) be the only one and + // reflect the plasma session run. + KDesktopFile desktopFile(filePaths.first()); + return desktopFile.desktopGroup().readEntry("X-KDE-PluginInfo-Version", QString()); +} + +QString SoftwareInfo::osType() const +{ + const int bits = QT_POINTER_SIZE == 8 ? 64 : 32; + + return QString::number(bits); +} diff --git a/kcms/info/softwareinfo.h b/kcms/info/softwareinfo.h new file mode 100644 index 00000000..be7bad47 --- /dev/null +++ b/kcms/info/softwareinfo.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2019 Jonah Brüchert + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include + +#ifndef SOFTWAREINFO_H +#define SOFTWAREINFO_H + +class SoftwareInfo : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString kernelRelease READ kernelRelease CONSTANT) + Q_PROPERTY(QString frameworksVersion READ frameworksVersion CONSTANT) + Q_PROPERTY(QString qtVersion READ qtVersion CONSTANT) + Q_PROPERTY(QString plasmaVersion READ plasmaVersion CONSTANT) + Q_PROPERTY(QString osType READ osType CONSTANT) + +public: + SoftwareInfo(QObject *parent = nullptr); + QString kernelRelease() const; + QString frameworksVersion() const; + QString qtVersion() const; + QString plasmaVersion() const; + QString osType() const; +}; + +#endif // SOFTWAREINFO_H diff --git a/kcms/powermanagement/CMakeLists.txt b/kcms/powermanagement/CMakeLists.txt new file mode 100644 index 00000000..a620b48e --- /dev/null +++ b/kcms/powermanagement/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileCopyrightText: 2020 Tomaz Canabrava + +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_mobile_power\") + +add_library(kcm_mobile_power MODULE + mobilepower.cpp + batterymodel.cpp + statisticsprovider.cpp +) + +target_link_libraries(kcm_mobile_power + Qt::DBus + Qt::Core + KF6::CoreAddons + KF6::I18n + KF6::QuickAddons + KF6::ConfigCore + KF6::Solid +) + +install(TARGETS kcm_mobile_power DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) + +kpackage_install_package(package kcm_mobile_power kcms) diff --git a/kcms/powermanagement/Messages.sh b/kcms/powermanagement/Messages.sh new file mode 100644 index 00000000..96a2b43f --- /dev/null +++ b/kcms/powermanagement/Messages.sh @@ -0,0 +1 @@ +$XGETTEXT $(find . -name \*.cpp -o -name \*.h -o -name \*.qml) -o $podir/kcm_mobile_powermanagement.pot diff --git a/kcms/powermanagement/batterymodel.cpp b/kcms/powermanagement/batterymodel.cpp new file mode 100644 index 00000000..92233161 --- /dev/null +++ b/kcms/powermanagement/batterymodel.cpp @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + * SPD + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "batterymodel.h" + +#include + +#include +#include + +BatteryModel::BatteryModel(QObject *parent) + : QAbstractListModel(parent) +{ + qmlRegisterUncreatableType("org.kde.kinfocenter.energy.private", 1, 0, "Battery", QStringLiteral("Use Solid::Battery")); + + m_batteries = Solid::Device::listFromType(Solid::DeviceInterface::Battery); + + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, [this](const QString &udi) { + auto it = std::find_if(m_batteries.constBegin(), m_batteries.constEnd(), [&udi](const Solid::Device &dev) { + return dev.udi() == udi; + }); + if (it != m_batteries.constEnd()) { + return; + } + + const Solid::Device device(udi); + if (device.isValid() && device.is()) { + beginInsertRows(QModelIndex(), m_batteries.count(), m_batteries.count()); + m_batteries.append(device); + endInsertRows(); + + Q_EMIT countChanged(); + } + }); + connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, [this](const QString &udi) { + auto it = std::find_if(m_batteries.constBegin(), m_batteries.constEnd(), [&udi](const Solid::Device &dev) { + return dev.udi() == udi; + }); + if (it == m_batteries.constEnd()) { + return; + } + + int index = std::distance(m_batteries.constBegin(), it); + + beginRemoveRows(QModelIndex(), index, index); + m_batteries.removeAt(index); + endRemoveRows(); + + Q_EMIT countChanged(); + }); +} + +QVariant BatteryModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_batteries.count()) { + return QVariant(); + } + + if (role == BatteryRole) { + // .as returns a pointer to a casted DeviceInterface. This pointer must + // not, under any circumstances, be deleted outside Solid! + // https://bugs.kde.org/show_bug.cgi?id=413003 + const auto battery = m_batteries.value(index.row()).as(); + QQmlEngine::setObjectOwnership(battery, QQmlEngine::CppOwnership); + return QVariant::fromValue(battery); + } else if (role == ProductRole) { + const Solid::Device device = m_batteries.value(index.row()); + return device.product(); + } else if (role == VendorRole) { + const Solid::Device device = m_batteries.value(index.row()); + return device.vendor(); + } else if (role == UdiRole) { + return m_batteries.at(index.row()).udi(); + } + + return QVariant(); +} + +int BatteryModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_batteries.count(); +} + +QHash BatteryModel::roleNames() const +{ + return {{BatteryRole, "battery"}, {VendorRole, "vendor"}, {ProductRole, "product"}, {UdiRole, "udi"}}; +} diff --git a/kcms/powermanagement/batterymodel.h b/kcms/powermanagement/batterymodel.h new file mode 100644 index 00000000..87e92ca8 --- /dev/null +++ b/kcms/powermanagement/batterymodel.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include + +#include +#include + +class BatteryModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + explicit BatteryModel(QObject *parent); + ~BatteryModel() override = default; + + enum Roles { + BatteryRole = Qt::UserRole, + UdiRole, + VendorRole, + ProductRole, + }; + Q_ENUM(Roles) + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +signals: + void countChanged(); + +private: + QList m_batteries; +}; diff --git a/kcms/powermanagement/mobilepower.cpp b/kcms/powermanagement/mobilepower.cpp new file mode 100644 index 00000000..b1355c93 --- /dev/null +++ b/kcms/powermanagement/mobilepower.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +// SPDX-FileCopyrightText: 2020 Tomaz Canabrava + +#include "mobilepower.h" +#include "statisticsprovider.h" + +#include +#include +#include +#include + +#include + +K_PLUGIN_CLASS_WITH_JSON(MobilePower, "powermanagement.json") + +enum { + THIRTY_SECONDS, + ONE_MINUTE, + TWO_MINUTES, + FIVE_MINUTES, + TEN_MINUTES, + FIFTEEN_MINUTES, + NEVER, +}; + +const QStringList timeValues = { + i18n("30 sec"), + i18n("1 min"), + i18n("2 min"), + i18n("5 min"), + i18n("10 min"), + i18n("15 min"), + i18n("Never"), +}; + +// Maps the indices of the timeValues indexes +// to minutes. +const QMap idxToMinutes = { + {THIRTY_SECONDS, 0.5}, + {ONE_MINUTE, 1}, + {TWO_MINUTES, 2}, + {FIVE_MINUTES, 5}, + {TEN_MINUTES, 10}, + {FIFTEEN_MINUTES, 15}, + {NEVER, 0}, +}; + +MobilePower::MobilePower(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, metaData, args) + , m_batteries{new BatteryModel(this)} + , m_profilesConfig{KSharedConfig::openConfig("powermanagementprofilesrc", KConfig::SimpleConfig | KConfig::CascadeConfig)} +{ + qmlRegisterUncreatableType("org.kde.kcm.power.mobile.private", 1, 0, "BatteryModel", QStringLiteral("Use BatteryModel")); + qmlRegisterUncreatableType("org.kde.kcm.power.mobile.private", 1, 0, "Battery", ""); + qmlRegisterType("org.kde.kcm.power.mobile.private", 1, 0, "HistoryModel"); + + setButtons(KQuickAddons::ConfigModule::NoAdditionalButton); + load(); +} + +MobilePower::~MobilePower() = default; + +// contents of powermanagementprofilesrc +// +// [Battery][SuspendSession] // our LockScreen +// idleTime=600000 +// suspendThenHibernate = enabled / disabled. +// suspendType=1 +// type 1 = sleep +// type 8 = shutdown +// type 32 = lock screen +// +// [Battery][DimDisplay] // our "Sleep Screen" +// idleTime=300000 +// Aparently KDE removes this group when it's false. +// + +void MobilePower::load() +{ + // we assume that the [AC], [Battery], and [LowBattery] groups have the same value + // (which is done by this kcm) + + KConfigGroup batteryGroup = m_profilesConfig->group("Battery"); + + if (batteryGroup.hasGroup("DimDisplay")) { + qDebug() << "[Battery][DimDisplay] group is listed"; + KConfigGroup dimSettings = batteryGroup.group("DimDisplay"); + + // powerdevil/dimdisplayconfig.cpp - here we load time / 60 / 1000 + // We should really, really, stop doing that. + m_dimScreenTime = (dimSettings.readEntry("idleTime").toDouble() / 60) / 1000; + } else { + qDebug() << "[Battery][DimDisplay] Group is not listed"; + m_dimScreenTime = 0; + } + + if (batteryGroup.hasGroup("DPMSControl")) { + qDebug() << "[Battery][DPMSControl] group is listed"; + KConfigGroup dpmsSettings = batteryGroup.group("DPMSControl"); + m_screenOffTime = dpmsSettings.readEntry("idleTime").toDouble() / 60 / 1000; + } else { + qDebug() << "[Battery][DPMSControl] is not listed"; + m_screenOffTime = 0; + } + + if (batteryGroup.hasGroup("SuspendSession")) { + qDebug() << "[Battery][SuspendSession] group is listed"; + KConfigGroup suspendSessionGroup = batteryGroup.group("SuspendSession"); + m_suspendSessionTime = suspendSessionGroup.readEntry("idleTime").toDouble() / 60 / 1000; + } else { + qDebug() << "[Battery][SuspendSession] is not listed"; + m_suspendSessionTime = 0; + } +} + +void MobilePower::save() +{ + // we set all profiles at the same time, since our UI is quite a simple global toggle + KConfigGroup acGroup = m_profilesConfig->group("AC"); + KConfigGroup batteryGroup = m_profilesConfig->group("Battery"); + KConfigGroup lowBatteryGroup = m_profilesConfig->group("LowBattery"); + + if (m_dimScreenTime == 0) { + qDebug() << "Deleting the group DimDisplay"; + + acGroup.deleteGroup("DimDisplay", KConfigGroup::Notify); + batteryGroup.deleteGroup("DimDisplay", KConfigGroup::Notify); + lowBatteryGroup.deleteGroup("DimDisplay", KConfigGroup::Notify); + } else { + // powerdevil/dimdisplayconfig.cpp - here we store time * 60 * 1000 + // We should really, really, stop doing that. + acGroup.group("DimDisplay").writeEntry("idleTime", m_dimScreenTime * 60 * 1000, KConfigGroup::Notify); + batteryGroup.group("DimDisplay").writeEntry("idleTime", m_dimScreenTime * 60 * 1000, KConfigGroup::Notify); + lowBatteryGroup.group("DimDisplay").writeEntry("idleTime", m_dimScreenTime * 60 * 1000, KConfigGroup::Notify); + } + + if (m_screenOffTime == 0) { + qDebug() << "Deleting the group DPMSControl"; + + acGroup.deleteGroup("DPMSControl", KConfigGroup::Notify); + batteryGroup.deleteGroup("DPMSControl", KConfigGroup::Notify); + lowBatteryGroup.deleteGroup("DPMSControl", KConfigGroup::Notify); + } else { + acGroup.group("DPMSControl").writeEntry("idleTime", m_screenOffTime * 60 * 1000, KConfigGroup::Notify); + batteryGroup.group("DPMSControl").writeEntry("idleTime", m_screenOffTime * 60 * 1000, KConfigGroup::Notify); + lowBatteryGroup.group("DPMSControl").writeEntry("idleTime", m_screenOffTime * 60 * 1000, KConfigGroup::Notify); + } + + // ensure the system is locked when the screen is turned off + acGroup.group("DPMSControl").writeEntry("lockBeforeTurnOff", 1, KConfigGroup::Notify); + batteryGroup.group("DPMSControl").writeEntry("lockBeforeTurnOff", 1, KConfigGroup::Notify); + lowBatteryGroup.group("DPMSControl").writeEntry("lockBeforeTurnOff", 1, KConfigGroup::Notify); + + if (m_suspendSessionTime == 0) { + qDebug() << "Deleting the group SuspendDisplay"; + + acGroup.deleteGroup("SuspendSession", KConfigGroup::Notify); + batteryGroup.deleteGroup("SuspendSession", KConfigGroup::Notify); + lowBatteryGroup.deleteGroup("SuspendSession", KConfigGroup::Notify); + } else { + acGroup.group("SuspendSession").writeEntry("idleTime", m_suspendSessionTime * 60 * 1000, KConfigGroup::Notify); + acGroup.group("SuspendSession").writeEntry("suspendType", 1, KConfigGroup::Notify); + + batteryGroup.group("SuspendSession").writeEntry("idleTime", m_suspendSessionTime * 60 * 1000, KConfigGroup::Notify); + batteryGroup.group("SuspendSession").writeEntry("suspendType", 1, KConfigGroup::Notify); + + lowBatteryGroup.group("SuspendSession").writeEntry("idleTime", m_suspendSessionTime * 60 * 1000, KConfigGroup::Notify); + lowBatteryGroup.group("SuspendSession").writeEntry("suspendType", 1, KConfigGroup::Notify); + } + + m_profilesConfig->sync(); + // Do not mess with Suspend Type + // suspendSessionGroup.writeEntry("suspendType", 32); // always lock screen. +} + +QStringList MobilePower::timeOptions() const +{ + return timeValues; +} + +void MobilePower::setDimScreenIdx(int idx) +{ + qreal value = idxToMinutes.value(idx); + qDebug() << "Got the value" << value; + + if (m_dimScreenTime == value) { + return; + } + + if (value == 0) { + qDebug() << "Setting to never dim"; + } else { + qDebug() << "Setting to dim in " << value << "Minutes"; + } + + m_dimScreenTime = value; + Q_EMIT dimScreenIdxChanged(); + save(); +} + +void MobilePower::setScreenOffIdx(int idx) +{ + qreal value = idxToMinutes.value(idx); + qDebug() << "Got the value" << value; + + if (m_screenOffTime == value) { + return; + } + + if (value == 0) { + qDebug() << "Setting to never screen off"; + } else { + qDebug() << "Setting to screen off in " << value << "Minutes"; + } + m_screenOffTime = value; + + Q_EMIT screenOffIdxChanged(); + save(); +} + +void MobilePower::setSuspendSessionIdx(int idx) +{ + qreal value = idxToMinutes.value(idx); + qDebug() << "Got the value" << value; + + if (m_suspendSessionTime == value) { + return; + } + + if (value == 0) { + qDebug() << "Setting to never suspend"; + } else { + qDebug() << "Setting to suspend in " << value << "Minutes"; + } + + m_suspendSessionTime = value; + Q_EMIT suspendSessionIdxChanged(); + save(); +} + +int MobilePower::suspendSessionIdx() +{ + if (m_suspendSessionTime == 0) { + return NEVER; + } else if (qFuzzyIsNull(m_suspendSessionTime)) { + return NEVER; + } else if (qFuzzyCompare(m_suspendSessionTime, 0.5)) { + return THIRTY_SECONDS; + } + + return idxToMinutes.key(std::round(m_suspendSessionTime)); +} + +int MobilePower::dimScreenIdx() +{ + if (m_dimScreenTime == 0) { + return NEVER; + } else if (qFuzzyIsNull(m_dimScreenTime)) { + return NEVER; + } else if (qFuzzyCompare(m_dimScreenTime, 0.5)) { + return THIRTY_SECONDS; + } + + return idxToMinutes.key(std::round(m_dimScreenTime)); +} + +int MobilePower::screenOffIdx() +{ + if (m_screenOffTime == 0) { + return NEVER; + } else if (qFuzzyIsNull(m_screenOffTime)) { + return NEVER; + } else if (qFuzzyCompare(m_screenOffTime, 0.5)) { + return THIRTY_SECONDS; + } + + return idxToMinutes.key(std::round(m_screenOffTime)); +} + +BatteryModel *MobilePower::batteries() +{ + return m_batteries; +} + +#include "mobilepower.moc" diff --git a/kcms/powermanagement/mobilepower.h b/kcms/powermanagement/mobilepower.h new file mode 100644 index 00000000..b7555fdf --- /dev/null +++ b/kcms/powermanagement/mobilepower.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +// SPDX-FileCopyrightText: 2020 Tomaz Canabrava + +#pragma once + +#include "batterymodel.h" + +#include +#include +#include + +class MobilePower : public KQuickAddons::ConfigModule +{ + Q_OBJECT + Q_PROPERTY(BatteryModel *batteries READ batteries CONSTANT) + Q_PROPERTY(int dimScreenIdx READ dimScreenIdx WRITE setDimScreenIdx NOTIFY dimScreenIdxChanged) + Q_PROPERTY(int screenOffIdx READ screenOffIdx WRITE setScreenOffIdx NOTIFY screenOffIdxChanged) + Q_PROPERTY(int suspendSessionIdx READ suspendSessionIdx WRITE setSuspendSessionIdx NOTIFY suspendSessionIdxChanged) + +public: + MobilePower(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args); + ~MobilePower(); + + Q_INVOKABLE QStringList timeOptions() const; + + void setDimScreenIdx(int idx); + void setScreenOffIdx(int idx); + void setSuspendSessionIdx(int idx); + int dimScreenIdx(); + int screenOffIdx(); + int suspendSessionIdx(); + + BatteryModel *batteries(); + + Q_SIGNAL void dimScreenIdxChanged(); + Q_SIGNAL void screenOffIdxChanged(); + Q_SIGNAL void suspendSessionIdxChanged(); + + QString stringForValue(int value); + + void load() override; + void save() override; + +private: + BatteryModel *m_batteries; + KSharedConfig::Ptr m_profilesConfig; + + qreal m_suspendSessionTime; + qreal m_dimScreenTime; + qreal m_screenOffTime; +}; diff --git a/kcms/powermanagement/package/contents/ui/BatteryPage.qml b/kcms/powermanagement/package/contents/ui/BatteryPage.qml new file mode 100644 index 00000000..cf58e050 --- /dev/null +++ b/kcms/powermanagement/package/contents/ui/BatteryPage.qml @@ -0,0 +1,185 @@ +/* + SPDX-FileCopyrightText: 2022 Devin Lin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.2 +import QtQuick.Controls 2.10 as QQC2 +import QtQuick.Layouts 1.11 + +import org.kde.kirigami 2.10 as Kirigami +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kcm 1.2 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.kcm.power.mobile.private 1.0 + +Kirigami.ScrollablePage { + id: root + + property QtObject battery + property string vendor + property string product + property string currentUdi + + title: i18n("Battery Information") + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + HistoryModel { + id: history + duration: 86400 // last 24 hours + device: currentUdi + type: HistoryModel.ChargeType + } + + ColumnLayout { + width: parent.width + spacing: 0 + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Usage Graph") + } + + MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + background: Item {} + clip: true + + contentItem: Flickable { + implicitWidth: 500 + implicitHeight: 200 + contentWidth: 500 + contentHeight: 200 + + Graph { + id: graph + width: 500 + height: 200 + implicitWidth: 500 + implicitHeight: 200 + data: history.points + + // Set grid lines distances which directly correspondent to the xTicksAt variables + readonly property var xDivisionWidths: [1000 * 60 * 10, 1000 * 60 * 60 * 12, 1000 * 60 * 60, 1000 * 60 * 30, 1000 * 60 * 60 * 2, 1000 * 60 * 10] + xTicksAt: graph.xTicksAtFullSecondHour + xDivisionWidth: xDivisionWidths[xTicksAt] + + xMin: history.firstDataPointTime + xMax: history.lastDataPointTime + xDuration: history.duration + + yUnits: i18nc("literal percent sign","%") + yMax: 100 + yStep: 20 + visible: history.count > 1 + } + } + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Information") + } + + MobileForm.FormTextDelegate { + id: isRechargeableDelegate + text: i18n("Is Rechargeable") + description: battery.rechargeable ? i18n("Yes") : i18n("No") + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: chargeStateDelegate + text: i18n("Charge State") + description: { + switch (battery.chargeState) { + case Battery.NoCharge: return i18n("Not charging") + case Battery.Charging: return i18n("Charging") + case Battery.Discharging: return i18n("Discharging") + case Battery.FullyCharged: return i18n("Fully charged") + default: return i18n("Unknown") + } + } + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: currentChargeDelegate + text: i18n("Current Charge") + description: i18nc("%1 is value, %2 is unit", "%1 %2", Number(battery.chargePercent).toLocaleString(Qt.locale(), "f", 0), i18n("%")) + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: healthDelegate + text: i18n("Health") + description: i18nc("%1 is value, %2 is unit", "%1 %2", Number(battery.capacity).toLocaleString(Qt.locale(), "f", 0), i18n("%")) + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: vendorDelegate + text: i18n("Vendor") + description: root.vendor + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: modelDelegate + text: i18n("Model") + description: root.product + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: serialDelegate + text: i18n("Serial Number") + description: battery.serial + } + + MobileForm.FormDelegateSeparator {} + + MobileForm.FormTextDelegate { + id: technologyDelegate + text: i18n("Technology") + description: { + switch (battery.technology) { + case Battery.LithiumIon: return i18n("Lithium ion") + case Battery.LithiumPolymer: return i18n("Lithium polymer") + case Battery.LithiumIronPhosphate: return i18n("Lithium iron phosphate") + case Battery.LeadAcid: return i18n("Lead acid") + case Battery.NickelCadmium: return i18n("Nickel cadmium") + case Battery.NickelMetalHydride: return i18n("Nickel metal hydride") + default: return i18n("Unknown technology") + } + } + } + } + } + } +} diff --git a/kcms/powermanagement/package/contents/ui/Graph.qml b/kcms/powermanagement/package/contents/ui/Graph.qml new file mode 100644 index 00000000..d2b89899 --- /dev/null +++ b/kcms/powermanagement/package/contents/ui/Graph.qml @@ -0,0 +1,243 @@ +/* + * SPDX-FileCopyrightText: 2015 David Edmundson + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +import QtQuick 2.3 + +/** + * We need to draw a graph, all other libs are not suitable as we are basically + * a connected scatter plot with non linear X spacing. + * Currently this is not available in kdeclarative nor kqtquickcharts + * + * We only paint once, so canvas is fast enough for our purposes. + * It is designed to look identical to those in ksysguard. + */ + +Canvas +{ + id: canvas + antialiasing: true + + readonly property real xTicksAtDontCare: 0 + readonly property real xTicksAtTwelveOClock: 1 + readonly property real xTicksAtFullHour: 2 + readonly property real xTicksAtHalfHour: 3 + readonly property real xTicksAtFullSecondHour: 4 + readonly property real xTicksAtTenMinutes: 5 + readonly property real xTicksAtFullTwoHours: 6 + + property int xPadding: 45 + property int yPadding: 40 + + property var data //expect an array of QPointF + + property real yMax: 100 + property real xMax: 100 + property real yMin: 0 + property real xMin: 0 + property real yStep: 20 + + property string yUnits: "" + property string xUnits: "" + + property real xDuration: 3600 + property real xDivisions: 6 + property real xDivisionWidth: 600000 + property real xTicksAt: xTicksAtDontCare + + //internal + + property real plotWidth: width - xPadding * 1.5 + property real plotHeight: height - yPadding * 2 + + onDataChanged: { + canvas.requestPaint(); + } + + //take a QPointF + function scalePoint(plot, currentUnixTime) { + var scaledX = (plot.x - (currentUnixTime / 1000 - xDuration)) / xDuration * plotWidth; + var scaledY = (plot.y - yMin) * plotHeight / (yMax - yMin); + + return Qt.point(xPadding + scaledX, + height - yPadding - scaledY); + } + + SystemPalette { + id: palette; + colorGroup: SystemPalette.Active + } + + + onPaint: { + var c = canvas.getContext('2d'); + + c.clearRect(0,0, width, height) + + //draw the background + c.fillStyle = palette.base + c.fillRect(xPadding, yPadding, plotWidth, plotHeight); + + //reset for fonts and stuff + c.fillStyle = palette.text + + //Draw the lines + + c.lineWidth = 1; + c.lineJoin = 'round'; + c.lineCap = 'round'; + c.strokeStyle = 'rgba(255, 0, 0, 1)'; + var gradient = c.createLinearGradient(0,0,0,height); + gradient.addColorStop(0, 'rgba(255, 0, 0, 0.2)'); + gradient.addColorStop(1, 'rgba(255, 0, 0, 0.05)'); + c.fillStyle = gradient; + + // For scaling + var currentUnixTime = Date.now() + var xMinUnixTime = currentUnixTime - xDuration * 1000 + + // Draw the line graph + c.beginPath(); + + var index = 0 + + while ((index < data.length - 1) && (data[index].x < (xMinUnixTime / 1000))) { + index++ + } + + var firstPoint = scalePoint(data[index], currentUnixTime) + c.moveTo(firstPoint.x, firstPoint.y) + + var point + for (var i = index + 1; i < data.length; i++) { + if (data[i].x > (xMinUnixTime / 1000)) { + point = scalePoint(data[i], currentUnixTime) + c.lineTo(point.x, point.y) + } + } + + c.stroke(); + c.strokeStyle = 'rgba(0, 0, 0, 0)'; + c.lineTo(point.x, height - yPadding); + c.lineTo(firstPoint.x, height - yPadding); + c.fill(); + + c.closePath() + + // Draw the frame on top + + //draw an outline + c.strokeStyle = 'rgba(0,50,0,0.02)'; + c.lineWidth = 1; + c.rect(xPadding - 1, yPadding - 1, plotWidth + 2, plotHeight + 2); + + // Draw the Y value texts + c.fillStyle = palette.text; + c.textAlign = "right" + c.textBaseline = "middle"; + for(var i = 0; i <= yMax; i += yStep) { + var y = scalePoint(Qt.point(0,i)).y; + + c.fillText(i + canvas.yUnits, xPadding - 10, y); + + //grid line + c.moveTo(xPadding, y) + c.lineTo(plotWidth + xPadding, y) + } + c.stroke() + + // Draw the X value texts + c.textAlign = "center" + c.lineWidth = 1 + c.strokeStyle = 'rgba(0, 0, 0, 0.15)' + + var xDivisions = xDuration / xDivisionWidth * 1000 + var xGridDistance = plotWidth / xDivisions + var xTickPos + var xTickDateTime + var xTickDateStr + var xTickTimeStr + + var currentDateTime = new Date() + var lastDateStr = currentDateTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + + var hours = currentDateTime.getHours() + var minutes = currentDateTime.getMinutes() + var seconds = currentDateTime.getSeconds() + + var diff + + switch (xTicksAt) { + case xTicksAtTwelveOClock: + diff = ((hours - 12) * 60 * 60 + minutes * 60 + seconds) + break + case xTicksAtFullHour: + diff = (minutes * 60 + seconds) + break + case xTicksAtFullSecondHour: + diff = (minutes * 60 + seconds) + break + case xTicksAtHalfHour: + diff = ((minutes - 30) * 60 + seconds) + break + case xTicksAtTenMinutes: + diff = ((minutes % 10) * 60 + seconds) + break + default: + diff = 0 + } + + var xGridOffset = plotWidth * (diff / xDuration) + var dateChanged = false + + var dashedLines = 50 + var dashedLineLength = plotHeight / dashedLines + var dashedLineDutyCycle + + for (var i = xDivisions; i >= -1; i--) { + xTickPos = i * xGridDistance + xPadding - xGridOffset + + if ((xTickPos > xPadding) && (xTickPos < plotWidth + xPadding)) + { + xTickDateTime = new Date(currentUnixTime - (xDivisions - i) * xDivisionWidth - diff * 1000) + xTickDateStr = xTickDateTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + xTickTimeStr = xTickDateTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + + if (lastDateStr != xTickDateStr) { + dateChanged = true + } + + if ((i % 2 == 0) || (xDivisions < 10)) + { + // Display the time + c.fillText(xTickTimeStr, xTickPos, canvas.height - yPadding / 2) + + // If the date has changed and is not the current day in a <= 24h graph, display it + // Always display the date for 48h and 1 week graphs + if (dateChanged || (xDuration > (60*60*48))) { + c.fillText(xTickDateStr, xTickPos, canvas.height - yPadding / 4) + dateChanged = false + } + + // Tick markers + c.moveTo(xTickPos, canvas.height - yPadding) + c.lineTo(xTickPos, canvas.height - (yPadding * 4) / 5) + + dashedLineDutyCycle = 0.5 + } else { + dashedLineDutyCycle = 0.1 + } + + for (var j = 0; j < dashedLines; j++) { + c.moveTo(xTickPos, yPadding + j * dashedLineLength) + c.lineTo(xTickPos, yPadding + j * dashedLineLength + dashedLineDutyCycle * dashedLineLength) + } + lastDateStr = xTickDateStr + } + } + c.stroke() + } +} diff --git a/kcms/powermanagement/package/contents/ui/main.qml b/kcms/powermanagement/package/contents/ui/main.qml new file mode 100644 index 00000000..74e2d47d --- /dev/null +++ b/kcms/powermanagement/package/contents/ui/main.qml @@ -0,0 +1,165 @@ +/* + SPDX-FileCopyrightText: 2011 Sebastian Kügler + SPDX-FileCopyrightText: 2012 Marco Martin + SPDX-FileCopyrightText: 2015 Kai Uwe Broulik + SPDX-FileCopyrightText: 2022 Devin Lin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.2 +import QtQuick.Controls 2.10 as QQC2 +import QtQuick.Layouts 1.11 + +import org.kde.kirigami 2.10 as Kirigami +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.kcm 1.2 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm +import org.kde.kcm.power.mobile.private 1.0 + +SimpleKCM { + id: powermanagementModule + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + ColumnLayout { + width: parent.width + spacing: 0 + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Devices") + } + + Repeater { + model: kcm.batteries + + delegate: MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + + onClicked: kcm.push("BatteryPage.qml", { "battery": model.battery, "vendor": model.vendor, "product": model.product, "currentUdi": model.udi }) + + contentItem: RowLayout { + spacing: Kirigami.Units.gridUnit + + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + Layout.rightMargin: Kirigami.Units.largeSpacing + source: { + switch (model.battery.type) { + case 3: return model.battery.chargeState === 1 ? "battery-full-charging" : "battery-full" + case 2: return "battery-ups" + case 9: return "monitor" + case 4: return "input-mouse" + case 5: return "input-keyboard" + case 1: return "phone" + case 7: return "smartphone" + default: return "paint-unknown" + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + Layout.fillWidth: true + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + color: Kirigami.Theme.textColor + text: { + let batteryType; + switch (model.battery.type) { + case 3: batteryType = i18n("Internal battery"); break; + case 2: batteryType = i18n("UPS battery"); break; + case 9: batteryType = i18n("Monitor battery"); break; + case 4: batteryType = i18n("Mouse battery"); break; + case 5: batteryType = i18n("Keyboard battery"); break; + case 1: batteryType = i18n("PDA battery"); break; + case 7: batteryType = i18n("Phone battery"); break; + default: batteryType = i18n("Unknown battery"); break; + } + + const chargePercent = i18nc("%1 is value, %2 is unit", "%1%2", Number(battery.chargePercent).toLocaleString(Qt.locale(), "f", 0), i18n("%")); + + return (model.battery.chargeState === Battery.Charging) ? i18nc("%1 is battery type, %2 is charge percent", "%1 %2 (Charging)", batteryType, chargePercent) : i18nc("%1 is battery type, %2 is charge percent", "%1 %2", batteryType, chargePercent); + } + } + + QQC2.ProgressBar { + Layout.fillWidth: true + from: 0 + to: 100 + value: model.battery.chargePercent + } + } + + Kirigami.Icon { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + source: "arrow-right" + implicitWidth: Math.round(Kirigami.Units.iconSizes.small * 0.75) + implicitHeight: Math.round(Kirigami.Units.iconSizes.small * 0.75) + } + } + } + } + } + } + + MobileForm.FormCard { + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.largeSpacing + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Screen") + } + + MobileForm.FormComboBoxDelegate { + id: dimScreenCombo + text: i18nc("Part of a sentence like 'Dim screen after 5 minutes'", "Dim screen after") + model: kcm.timeOptions() + currentIndex: kcm.dimScreenIdx + Component.onCompleted: dialog.parent = powermanagementModule + onCurrentIndexChanged: kcm.dimScreenIdx = currentIndex + } + + MobileForm.FormDelegateSeparator { above: dimScreenCombo; below: screenOffCombo } + + MobileForm.FormComboBoxDelegate { + id: screenOffCombo + text: i18nc("Part of a sentence like 'Turn off screen after 5 minutes'", "Turn off screen after") + model: kcm.timeOptions() + currentIndex: kcm.screenOffIdx + Component.onCompleted: dialog.parent = powermanagementModule + onCurrentIndexChanged: kcm.screenOffIdx = currentIndex + } + + MobileForm.FormDelegateSeparator { above: screenOffCombo; below: suspendCombo } + + MobileForm.FormComboBoxDelegate { + id: suspendCombo + text: i18nc("Part of a sentence like 'Suspend device after 5 minutes'", "Suspend device after") + model: kcm.timeOptions() + currentIndex: kcm.suspendSessionIdx + Component.onCompleted: dialog.parent = powermanagementModule + onCurrentIndexChanged: kcm.suspendSessionIdx = currentIndex + } + } + } + } +} diff --git a/kcms/powermanagement/powermanagement.json b/kcms/powermanagement/powermanagement.json new file mode 100644 index 00000000..9156aaaf --- /dev/null +++ b/kcms/powermanagement/powermanagement.json @@ -0,0 +1,111 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-system;", + "KPlugin": { + "Description": "Configure power management settings", + "Description[az]": "Enerji sərfiyyatı ayarlarını tənzimlə", + "Description[ca@valencia]": "Configureu els paràmetres de gestió de l'energia", + "Description[ca]": "Configuració dels paràmetres de gestió d'energia", + "Description[cs]": "Konfigurace nastavení správy napájení", + "Description[en_GB]": "Configure power management settings", + "Description[es]": "Configurar las preferencias de la gestión de energía", + "Description[eu]": "Konfiguratu energia-kudeaketa ezarpenak", + "Description[fi]": "Aseta virranhallinta", + "Description[fr]": "Configurer les paramètres de gestion de l'énergie ", + "Description[ia]": "Configura preferentias de gestion de energia", + "Description[is]": "Grunnstilla orkunotkun", + "Description[it]": "Configura le impostazioni di gestione energetica", + "Description[ka]": "ენერგიის მართვის პარამეტრების კონფიგურაცია", + "Description[ko]": "전력 관리 설정", + "Description[lt]": "Konfigūruoti energijos valdymo nuostatas", + "Description[nl]": "Energie-instellingen configureren", + "Description[nn]": "Set opp straumstyring", + "Description[pa]": "ਪਾਵਰ ਇੰਤਜ਼ਾਮ ਸੈਟਿੰਗਾਂ ਦੀ ਸੰਰਚਨਾ", + "Description[pl]": "Ustawienia zarządzania energią", + "Description[pt]": "Configurar as definições de gestão de energia", + "Description[pt_BR]": "Configurar as opções do gerenciamento de energia", + "Description[ru]": "Настройка параметров энергопотребления", + "Description[sl]": "Konfiguriraj nastavitve upravljanja energijo", + "Description[sv]": "Anpassa inställningar av strömhantering", + "Description[tr]": "Güç yönetimi ayarlarını yapılandır", + "Description[uk]": "Налаштовування керування живленням", + "Description[vi]": "Cấu hình thiết lập quản lí nguồn điện", + "Description[x-test]": "xxConfigure power management settingsxx", + "Description[zh_CN]": "配置电源管理设置。", + "FormFactors": [ + "handset", + "tablet", + "mediacenter" + ], + "Icon": "preferences-system-power-management", + "Name": "Energy", + "Name[az]": "Enerji sərfiyyatı", + "Name[ca@valencia]": "Energia", + "Name[ca]": "Energia", + "Name[cs]": "Energie", + "Name[en_GB]": "Energy", + "Name[es]": "Energía", + "Name[eu]": "Energia", + "Name[fi]": "Virta", + "Name[fr]": "Énergie", + "Name[ia]": "Energia", + "Name[is]": "Orka", + "Name[it]": "Energia", + "Name[ka]": "ენერგია", + "Name[ko]": "에너지", + "Name[lt]": "Energija", + "Name[nl]": "Energie", + "Name[nn]": "Energibruk", + "Name[pa]": "ਊਰਜਾ", + "Name[pl]": "Energia", + "Name[pt]": "Energia", + "Name[pt_BR]": "Energia", + "Name[ru]": "Энергопотребление", + "Name[sl]": "Energija", + "Name[sv]": "Energi", + "Name[tr]": "Enerji", + "Name[uk]": "Живлення", + "Name[vi]": "Năng lượng", + "Name[x-test]": "xxEnergyxx", + "Name[zh_CN]": "电量" + }, + "X-KDE-Keywords": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver", + "X-KDE-Keywords[ast]": "videu,monitor,gráficos,dormir,dormición,bloquiar,bloquéu,curiapantalles,bloquiador de pantalla", + "X-KDE-Keywords[az]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver,qrafiklər,boşdayanma,yuxu,kilid,ekran kilidləyici,ekran qoruyucu", + "X-KDE-Keywords[ca@valencia]": "vídeo, monitor, gràfics, temps d'espera, adorm, bloqueja, bloqueig de la pantalla, estalvi de pantalla", + "X-KDE-Keywords[ca]": "vídeo, monitor, gràfics, temps d'espera, adorm, bloqueja, bloqueig de la pantalla, estalvi de pantalla", + "X-KDE-Keywords[cs]": "video, monitor, grafika, časový limit, spánek, zámek, zamčení obrazovky, spořič obrazovky", + "X-KDE-Keywords[da]": "video, skærm, grafik, tidsudløb, slumre, lås, skærmlåsning, pauseskærm", + "X-KDE-Keywords[de]": "Video,Monitor,Grafik,Zeitüberschreitung,Standby,Sperren, Bildschirmsperre,Bildschirmschoner", + "X-KDE-Keywords[el]": "βίντεο, οθόνη, γραφικά, χρονικό όριο, ύπνωση, κλείδωμα, κλείδωμα οθόνης, προστασία οθόνης", + "X-KDE-Keywords[en_GB]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver", + "X-KDE-Keywords[es]": "vídeo, monitor, gráficos, tiempo de espera, dormir, bloquear, bloqueo de pantalla, salvapantallas", + "X-KDE-Keywords[et]": "video, monitor, graafika, aegumine, uni, lukustus, ekraanisäästja", + "X-KDE-Keywords[eu]": "bideoa, monitorea, grafikoak, denbora-muga, egin lo, giltzatu, pantaila-giltzatzailea, pantaila-babeslea", + "X-KDE-Keywords[fi]": "video, näyttö, grafiikka, aikakatkaisu, keskeytystila, lukitus, näyttölukko, näytönsäästäjä", + "X-KDE-Keywords[fr]": "vidéo, écran, graphique, mise en veille, sommeil, verrouillage, verrouillage d'écran, écran de veille", + "X-KDE-Keywords[gl]": "vídeo, monitor, gráficos, tempo límite, hibernar, bloquear, bloqueador de pantalla, salvapantallas", + "X-KDE-Keywords[hu]": "videó, monitor, grafika, időkorlát, alvás, zárolás, képernyőzár, képernyővédő", + "X-KDE-Keywords[ia]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver", + "X-KDE-Keywords[it]": "video, monitor, grafica, tempo massimo, attendi, blocca, bloccaschermo, salvaschermo", + "X-KDE-Keywords[ko]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver, 비디오, 모니터, 그래픽, 시간 제한, 절전, 대기, 잠금, 화면 보호기, 화면 잠금", + "X-KDE-Keywords[lt]": "vaizdas, monitorius, vaizduoklis, grafika, laiko limitas, miegas, miegoti, užraktas, uzraktas, užrakinti, uzrakinti, ekrano užraktas, ekrano uzraktas, ekrano užsklanda, ekrano uzsklanda", + "X-KDE-Keywords[nl]": "video, monitor, grafisch, tijdslimiet, onderbreken, vergrendelen, schermvergrendelaar, schermbeveiliging", + "X-KDE-Keywords[nn]": "video, skjerm, grafikk, tidsavbrot, timeout, sove, lås, låsing, skjermlåsar, pauseskjerm, skjermsparar", + "X-KDE-Keywords[pa]": "ਵੀਡੀਓ, ਮਾਨੀਟਰ, ਗਰਾਫਿਕਸ, ਟਾਈਮ ਆਉਟ, ਸਲੀਪ, ਲਾਕ, ਸਕਰੀਨ-ਲਾਕਰ, ਸਕਰੀਨ-ਸੇਵਰ", + "X-KDE-Keywords[pl]": "video, monitor, grafika, timeout, uśpij, zablokuj, blokada ekranu, wygaszacz ekranu", + "X-KDE-Keywords[pt]": "vídeo, monitor, gráfica, tempo-limite, suspender, bloquear, bloqueio de ecrã, protector de ecrã", + "X-KDE-Keywords[pt_BR]": "vídeo, monitor, gráfico, tempo limite, suspender, bloquear, bloqueio de tela, protetor de tela", + "X-KDE-Keywords[ro]": "video, monitor, grafică, expirare, somn, adormire, blocare, blocare ecran, protecție ecran", + "X-KDE-Keywords[ru]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver, видео, монитор, экран, графика, блокировка", + "X-KDE-Keywords[sk]": "video, obrazovka, grafika, prestávka, spánok, zámok, zámok obrazovky, šetrič obrazovky", + "X-KDE-Keywords[sl]": "video, zaslon, grafika, časovna omejitev, spanje, zaklep, zaklep zaslona, ohranjevalnik zaslona", + "X-KDE-Keywords[sv]": "video, bildskärm, grafik, tidsgräns, viloläge, låsning, skärmlåsning, skärmsläckare", + "X-KDE-Keywords[tr]": "video, monitör, grafikler, zaman aşımı, uyku, kilit, ekran kilitleyici, ekran koruyucu", + "X-KDE-Keywords[uk]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver, відео, монітор, графіка, час очікування, присипляння, блокування, збереження, екран", + "X-KDE-Keywords[vi]": "video,monitor,graphics,timeout,sleep,lock,screenlocker,screensaver,phim,màn hình,đồ hoạ,tắt màn hình,ngủ,khoá,khoá màn hình,bảo vệ màn hình", + "X-KDE-Keywords[x-test]": "xxvideoxx,xx monitorxx,xx graphicsxx,xx timeoutxx,xx sleepxx,xx lockxx,xx screenlockerxx,xx screensaverxx", + "X-KDE-Keywords[zh_CN]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver, 视频, 显示器, 监视器, 图形, 图像, 超时, 睡眠, 锁屏, 屏幕保护, 屏幕锁定, 屏保, 锁定", + "X-KDE-Keywords[zh_TW]": "video, monitor, graphics, timeout, sleep, lock, screenlocker, screensaver, 影片, 顯示器, 圖形, 顯示卡, 逾時, 休眠, 鎖定, 螢幕鎖定, 螢幕保護程式", + "X-KDE-System-Settings-Parent-Category": "display", + "X-KDE-Weight": 70 +} diff --git a/kcms/powermanagement/statisticsprovider.cpp b/kcms/powermanagement/statisticsprovider.cpp new file mode 100644 index 00000000..1f933c2d --- /dev/null +++ b/kcms/powermanagement/statisticsprovider.cpp @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: 2015 David Edmundson + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#include "statisticsprovider.h" + +#include +#include +#include +#include +#include // qDBusRegisterMetaType +#include +#include + +const QDBusArgument &operator<<(QDBusArgument &argument, const HistoryReply &data) +{ + argument.beginStructure(); + argument << data.time << data.value << data.charging; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, HistoryReply &attrs) +{ + arg.beginStructure(); + arg >> attrs.time >> attrs.value >> attrs.charging; + arg.endStructure(); + return arg; +} + +StatisticsProvider::StatisticsProvider(QObject *parent) + : QObject(parent) +{ + m_type = StatisticsProvider::ChargeType; + m_duration = 120; + + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); +} + +void StatisticsProvider::setDevice(const QString &device) +{ + if (device == m_device) { + return; + } + + m_device = device; + Q_EMIT deviceChanged(); + + load(); +} + +void StatisticsProvider::setDuration(uint duration) +{ + if (duration == m_duration) { + return; + } + + m_duration = duration; + Q_EMIT durationChanged(); + + load(); +} + +void StatisticsProvider::setType(StatisticsProvider::HistoryType type) +{ + if (m_type == type) { + return; + } + + m_type = type; + Q_EMIT typeChanged(); + + load(); +} + +void StatisticsProvider::classBegin() +{ +} + +void StatisticsProvider::componentComplete() +{ + m_isComplete = true; + load(); +} + +QVariantList StatisticsProvider::asPoints() const +{ + QVariantList points; + points.reserve(m_values.count()); + foreach (const HistoryReply &h, m_values) { + points.append(QPointF(h.time, h.value)); + } + + if (!points.isEmpty()) { + points.takeLast(); + } + + return points; +} + +int StatisticsProvider::count() const +{ + return m_values.count(); +} + +int StatisticsProvider::firstDataPointTime() const +{ + if (m_values.isEmpty()) { + return 0; + } + + return m_values.first().time; +} + +int StatisticsProvider::lastDataPointTime() const +{ + if (m_values.isEmpty()) { + return 0; + } + + return m_values.last().time; +} + +int StatisticsProvider::largestValue() const +{ + if (m_values.isEmpty()) { + return 0; + } + + int max = 0; // TODO std::max or something? + for (auto it = m_values.constBegin(), end = m_values.constEnd(); it != end; ++it) { + if ((*it).value > max) { + max = (*it).value; + } + } + return max; +} + +void StatisticsProvider::refresh() +{ + load(); +} + +void StatisticsProvider::load() +{ + if (!m_isComplete || m_device.isEmpty()) { + return; + } + + auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.UPower"), + m_device, + QStringLiteral("org.freedesktop.UPower.Device"), + QStringLiteral("GetHistory")); + if (m_type == RateType) { + msg << QLatin1String("rate"); + } else { // m_type must = ChargeType + msg << QLatin1String("charge"); + } + + uint resolution = 100; + msg << m_duration << resolution; + + QDBusPendingReply> reply = QDBusConnection::systemBus().asyncCall(msg); + + auto *watcher = new QDBusPendingCallWatcher(reply, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply> reply = *watcher; + watcher->deleteLater(); + m_values.clear(); + + if (reply.isError()) { + qWarning() << "Failed to get device history from UPower" << reply.error().message(); + return; + } + + foreach (const HistoryReply &r, reply.value()) { + if (r.value > 0) { // we get back some values which contain no value, possibly to indicate if charging changes, ignore them + m_values.prepend(r); + } + } + + Q_EMIT dataChanged(); + }); +} diff --git a/kcms/powermanagement/statisticsprovider.h b/kcms/powermanagement/statisticsprovider.h new file mode 100644 index 00000000..186add44 --- /dev/null +++ b/kcms/powermanagement/statisticsprovider.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2015 David Edmundson + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + */ + +#ifndef STATISTICSPROVIDER_H +#define STATISTICSPROVIDER_H + +#include +#include +#include + +struct HistoryReply { +public: + uint time = 0; + double value = 0.0; + uint charging = 0; +}; + +Q_DECLARE_METATYPE(HistoryReply) + +class StatisticsProvider : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + Q_PROPERTY(QString device MEMBER m_device WRITE setDevice NOTIFY deviceChanged) + Q_PROPERTY(uint duration MEMBER m_duration WRITE setDuration NOTIFY durationChanged) + Q_PROPERTY(HistoryType type MEMBER m_type WRITE setType NOTIFY typeChanged) + + Q_PROPERTY(QVariantList points READ asPoints NOTIFY dataChanged) + Q_PROPERTY(int count READ count NOTIFY dataChanged) + Q_PROPERTY(int firstDataPointTime READ firstDataPointTime NOTIFY dataChanged) + Q_PROPERTY(int lastDataPointTime READ lastDataPointTime NOTIFY dataChanged) + Q_PROPERTY(int largestValue READ largestValue NOTIFY dataChanged) + +public: + enum HistoryType { + RateType, + ChargeType, + }; + Q_ENUM(HistoryType) + + enum HistoryRoles { + TimeRole = Qt::UserRole + 1, + ValueRole, + ChargingRole, + }; + + explicit StatisticsProvider(QObject *parent = nullptr); + + void setDevice(const QString &device); + void setDuration(uint duration); + void setType(HistoryType type); + + void load(); + + void classBegin() override; + void componentComplete() override; + + QVariantList asPoints() const; + int count() const; + + int firstDataPointTime() const; + int lastDataPointTime() const; + int largestValue() const; + +Q_SIGNALS: + void deviceChanged(); + void typeChanged(); + void durationChanged(); + + void dataChanged(); + +public Q_SLOTS: + void refresh(); + +private: + QString m_device; + HistoryType m_type; + uint m_duration; // in seconds + + QList m_values; + bool m_isComplete = false; +}; + +#endif // STATISTICSPROVIDER_H diff --git a/kcms/time/CMakeLists.txt b/kcms/time/CMakeLists.txt new file mode 100644 index 00000000..f3171122 --- /dev/null +++ b/kcms/time/CMakeLists.txt @@ -0,0 +1,23 @@ +set(timesettings_SRCS + timesettings.cpp + timezonemodel.cpp + timezonesi18n.cpp +) + +qt_add_dbus_interface(timesettings_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/timedated1.xml timedated_interface) + +add_library(kcm_mobile_time MODULE ${timesettings_SRCS}) + +target_link_libraries(kcm_mobile_time + Qt::Quick + Qt::Qml + Qt::DBus + KF6::QuickAddons + KF6::ConfigCore + KF6::I18n +) + +# Time and Date + +install(TARGETS kcm_mobile_time DESTINATION ${KDE_INSTALL_QTPLUGINDIR}/kcms) +kpackage_install_package(package kcm_mobile_time kcms) diff --git a/kcms/time/Messages.sh b/kcms/time/Messages.sh new file mode 100644 index 00000000..e65d2e8a --- /dev/null +++ b/kcms/time/Messages.sh @@ -0,0 +1 @@ +$XGETTEXT $(find . -name \*.cpp -o -name \*.h -o -name \*.qml) -o $podir/kcm_mobile_time.pot diff --git a/kcms/time/clockconfig_actions.actions b/kcms/time/clockconfig_actions.actions new file mode 100644 index 00000000..7734fb12 --- /dev/null +++ b/kcms/time/clockconfig_actions.actions @@ -0,0 +1,128 @@ +[Domain] +Name=Date and Time Control Module +Name[az]=Tarix və Vaxta Nəzarət Modulu +Name[ca]=Mòdul de control de la data i hora +Name[ca@valencia]=Mòdul de control de la data i hora +Name[cs]=Ovládací modul data a času +Name[da]=Dato og klokkeslæt-kontrolmodul +Name[de]=Kontrollmodul für Datum und Zeit +Name[el]=Άρθρωμα ελέγχου ημερομηνίας και ώρας +Name[en_GB]=Date and Time Control Module +Name[es]=Módulo de control de fecha y hora +Name[et]=Kuupäeva ja kellaaja juhtimismoodul +Name[eu]=Data eta ordua aginte modulua +Name[fi]=Aika-asetukset +Name[fr]=Module de contrôle de date et d'heure +Name[gl]=Módulo de control da data e hora +Name[hu]=Dátum és idő beállítómodul +Name[ia]=Modulo de controlo de data e tempore +Name[id]=Modul Kontrol Waktu dan Tanggal +Name[is]=Stjórneining fyrir dagsetningu og tíma +Name[it]=Modulo di controllo Data e ora +Name[ka]=თარიღისა და დროის კონტროლის მოდული +Name[ko]=날짜와 시간 제어 모듈 +Name[lt]=Datos ir laiko valdymo modulis +Name[nl]=Controlemodule van datum en tijd +Name[nn]=Kontrollmodul for dato og klokkeslett +Name[pa]=ਮਿਤੀ ਅਤੇ ਟਾਈਮ ਕੰਟਰੋਲ ਮੋਡੀਊਲ +Name[pl]=Moduł sterowania datą i czasem +Name[pt]=Módulo de Controlo da Data e Hora +Name[pt_BR]=Módulo de controle da data e hora +Name[ro]=Modul de control pentru dată și oră +Name[ru]=Модуль настройки даты и времени +Name[sk]=Modul pre nastavenie dátumu a času +Name[sl]=Nadzorni modul za datum in čas +Name[sv]=Inställningsmodul för datum och tid +Name[ta]=தேதி மற்றும் நேரத்தை கட்டுப்படுத்தும் கூறு +Name[tr]=Tarih ve Zaman Denetim Modülü +Name[uk]=Модуль керування датою і часом +Name[vi]=Khối điều khiển ngày giờ +Name[x-test]=xxDate and Time Control Modulexx +Name[zh_CN]=日期和时间控制模块 +Name[zh_TW]=日期與時間控制模組 +Icon=preferences-system-time + +[org.kde.active.clockconfig.save] +Name=Save the date/time settings +Name[az]=Tarix/Vaxt ayarlarını saxlamaq +Name[ca]=Desa la configuració de la data i hora +Name[ca@valencia]=Guarda la configuració de la data i hora +Name[cs]=Uložit nastavení data a času +Name[da]=Gem dato-/klokkeslætsindstillingerne +Name[de]=Einstellungen für Datum und Zeit speichern +Name[el]=Αποθήκευση των ρυθμίσεων ημερομηνίας/ώρας +Name[en_GB]=Save the date/time settings +Name[es]=Guardar las preferencias de fecha y hora +Name[et]=Kuupäeva/kellaaja seadistuste salvestamine +Name[eu]=Gorde data/ordu ezarpenak +Name[fi]=Tallenna päiväyksen ja ajan asetukset +Name[fr]=Enregistrement des paramètres de date / d'heure +Name[gl]=Garda a configuración da data e hora +Name[hu]=A dátum- és időbeállítások mentése +Name[ia]=Salveguarda le preferentias de data/ tempore +Name[id]=Simpan pengaturan waktu/tanggal +Name[is]=Vista stillingar dagsetningar og tíma +Name[it]=Salva le impostazioni di data e ora +Name[ka]=თარიღის/დროის პარამეტრების შენახვა +Name[ko]=날짜와 시간 설정 저장 +Name[lt]=Įrašyti datos/laiko nuostatas +Name[nl]=Datum- en tijdinstellingen opslaan +Name[nn]=Lagra innstillingane for dato og klokkeslett +Name[pa]=ਮਿਤੀ/ਸਮਾਂ ਸੈਟਿੰਗਾਂ ਸੰਭਾਲੋ +Name[pl]=Zapisz ustawienia daty/czasu +Name[pt]=Mudar a configuração da data/hora +Name[pt_BR]=Salvar as configurações de data e hora +Name[ro]=Salvează configurările datei și orei +Name[ru]=Сохранить параметры даты и времени +Name[sk]=Uložiť nastavenia dátumu a času +Name[sl]=Shranjevanje nastavitev datuma in časa +Name[sv]=Spara inställningar av datum och tid +Name[ta]=தேதி மற்றும் நேர அமைப்புகளை சேமியுங்கள் +Name[tr]=Tarih/zaman ayarlarını kaydet +Name[uk]=Зберегти параметри дати/часу +Name[vi]=Lưu thiết lập ngày/giờ +Name[x-test]=xxSave the date/time settingsxx +Name[zh_CN]=保存日期/时间设置 +Name[zh_TW]=儲存日期與時間設定 +Description=System policies prevent you from saving the date/time settings. +Description[az]=Təhlükəsizlik qaydaları sizə tarix və vaxtı idarə etməyə mane olur. +Description[ca]=Les polítiques del sistema impedeixen que deseu la configuració de la data i hora. +Description[ca@valencia]=Les polítiques del sistema impedixen que guardeu la configuració de la data i hora. +Description[cs]=Nastavení systému vám znemožňuje uložit datum/čas. +Description[da]=Systempolitikker forhindrer dig i at gemme dato-/klokkeslætsindstillinger. +Description[de]=Die Einstellungen für Datum und Zeit können aufgrund einer Systemrichtlinie nicht gespeichert werden. +Description[el]=Οι πολιτικές του συστήματος σάς εμποδίζουν να αποθηκεύσετε τις ρυθμίσεις ημερομηνίας/ώρας. +Description[en_GB]=System policies prevent you from saving the date/time settings. +Description[es]=La política del sistema le impide que pueda guardar las preferencias de fecha y hora. +Description[et]=Süsteemi reeglid takistavad sul kuupäeva/kellaaja seadistusi salvestamast. +Description[eu]=Sistemako gidalerroek data/ordu ezarpenak gordetzea eragozten dizute. +Description[fi]=Järjestelmäkäytäntö estää sinua tallentamasta aika-asetuksia. +Description[fr]=Les stratégies système vous empêchent d'enregistrer les paramètres de date / d'heure. +Description[gl]=As políticas do sistema non permiten que garde a configuración da data e hora. +Description[hu]=A rendszer házirendjei nem engedik Önnek a dátum- és időbeállítások mentését. +Description[ia]=Le politicas de systema preveni te ex salveguardar le preferentias de tempore/data. +Description[id]=Kebijakan sistem menghalangi anda untuk menyimpan pengaturan waktu/tanggal. +Description[is]=Öryggisreglur kerfisins koma í veg fyrir að þú getir vistað stillingar fyrir tíma/dagsetningu. +Description[it]=Le politiche di sistema ti impediscono di salvare le impostazioni di data e ora. +Description[ka]=სისტემის წესები გიკრძალავთ დროის/თარიღის პარამეტრების შენახვას. +Description[ko]=시스템 정책 때문에 날짜와 시간 설정을 저장할 수 없습니다. +Description[lt]=Sistemos politika neleidžia jums įrašyti datos/laiko nustatymų. +Description[nl]=Systeembeleid voorkwam dat u de datum- en tijdinstellingen kon opslaan. +Description[nn]=Systemreglane hindrar deg i å lagra innstillingane for dato og klokkeslett. +Description[pa]=ਸਿਸਟਮ ਪਾਲਸੀ ਤੁਹਾਨੂੰ ਮਿਤੀ/ਸਮਾਂ ਸੈਟਿੰਗ ਬਦਲਣ ਤੋਂ ਤੁਹਾਨੂੰ ਰੋਕਦੀ ਹੈ। +Description[pl]=Polityka systemu nie pozwala ci na zapisanie ustawień daty/czasu. +Description[pt]=As políticas do sistema proíbem a modificação da configuração de data/hora. +Description[pt_BR]=As políticas do sistema não permitem a modificação das configurações de data e hora. +Description[ro]=Politicile sistemului vă interzic salvarea configurărilor de dată și oră. +Description[ru]=Правила безопасности запрещают вам управлять системной датой и временем. +Description[sk]=Systémové politiky vám zabránili uložiť nastavenie dátumu a času. +Description[sl]=Sistemski pravilniki vam onemogočajo, da bi shranili nastavitve datuma in časa. +Description[sv]=Systemets policy förhindrar att du sparar inställningar av datum och tid. +Description[ta]=தேதி மற்றும் நேர அமைப்புகளை நீங்கள் சேமிப்பதை கணினியின் கொள்கைகள் தடுக்கின்றன. +Description[tr]=Sistem politikaları, tarih/saat ayarlarını kaydetmenizi engeller. +Description[uk]=Відповідно до загальносистемних правил, ви не можете зберігати параметри дати/часу. +Description[vi]=Các chính sách của hệ thống không cho phép bạn lưu thiết lập ngày/giờ. +Description[x-test]=xxSystem policies prevent you from saving the date/time settings.xx +Description[zh_CN]=系统安全策略不允许您保存日期/时间设置。 +Description[zh_TW]=系統政策拒絕讓您儲存日期與時間設定。 +Policy=yes diff --git a/kcms/time/config.h.cmake b/kcms/time/config.h.cmake new file mode 100644 index 00000000..638d0da3 --- /dev/null +++ b/kcms/time/config.h.cmake @@ -0,0 +1,9 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +/* KDE's configuration directory */ +#define KDE_CONFDIR "${CONFIG_INSTALL_DIR}" + diff --git a/kcms/time/package/contents/ui/DatePicker.qml b/kcms/time/package/contents/ui/DatePicker.qml new file mode 100644 index 00000000..2ecf5dde --- /dev/null +++ b/kcms/time/package/contents/ui/DatePicker.qml @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 +import org.kde.kirigami 2.5 as Kirigami +//import "private" + +//FIXME: shouldn't be a FrameSvgItem +Item { + id: root + clip: true + + //////// API + property int day + property int month + property int year + + property bool userConfiguring: false + + property string isoDate: year + "-" + clockRow.twoDigitString(month) + "-" + clockRow.twoDigitString(day) + + property int fontSize: 14 + property int _margin: Kirigami.Units.gridUnit + + opacity: enabled ? 1.0 : 0.5 + + Rectangle { + color: "transparent" + border.width: 1 + border.color: Kirigami.Theme.textColor + anchors.fill: parent + opacity: 0.3 + } + + /////// Implementation + Connections { + target: root + onDayChanged: clockRow.day = root.day + onMonthChanged: clockRow.month = root.month + onYearChanged: clockRow.year = root.year + } + + + //imagePath: "widgets/picker" + width: clockRow.width + root._margin * 2 + height: clockRow.height + root._margin * 2 + + + Timer { + id: userConfiguringTimer + repeat: false + interval: 1500 + running: false + onTriggered: { + root.day = clockRow.day + root.month = clockRow.month + root.year = clockRow.year + userConfiguring = false + } + } + + Row { + id: clockRow + spacing: 3 + x: root._margin + y: root._margin + + property int day + property int month + property int year + + function twoDigitString(number) + { + return number < 10 ? "0"+number : number + } + + Digit { + id: dayDigit + model: { + var dd = new Date(year, month, 0); + return dd.getDate() + } + currentIndex: ((day - 1) < model) ? day-1 : 1 + onSelectedIndexChanged: { + if (selectedIndex > -1) { + day = selectedIndex+1 + } + } + delegate: Text { + horizontalAlignment: Text.AlignHCenter + width: dayDigit.width + property int ownIndex: index + text: index+1 + color: Kirigami.Theme.textColor + font.pointSize: root.fontSize + opacity: PathView.itemOpacity + } + } + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + } + } + Digit { + id: monthDigit + model: 12 + currentIndex: month -1 + onSelectedIndexChanged: { + if (selectedIndex > -1) { + month = selectedIndex + 1 + } + } + delegate: Text { + horizontalAlignment: Text.AlignHCenter + width: monthDigit.width + property int ownIndex: index + property variant months: Array(i18n("Jan"), i18n("Feb"), i18n("Mar"), i18n("Apr"), i18n("May"), i18n("Jun"), i18n("Jul"), i18n("Aug"), i18n("Sep"), i18n("Oct"), i18n("Nov"), i18n("Dec")) + text: months[index] + font.pointSize: root.fontSize + color: Kirigami.Theme.textColor + opacity: PathView.itemOpacity + } + width: monthPlaceHolder.width + Text { + id: monthPlaceHolder + visible: false + font.pointSize: root.fontSize + text: "0000" + } + } + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + } + } + Digit { + id: yearDigit + //FIXME: yes, this is a tad lame ;) + model: 3000 + currentIndex: year + onSelectedIndexChanged: { + if (selectedIndex > -1) { + year = selectedIndex + } + } + width: yearPlaceHolder.width*1.3 + Text { + id: yearPlaceHolder + visible: false + font.pointSize: root.fontSize + text: "0000" + } + } + } +} diff --git a/kcms/time/package/contents/ui/Digit.qml b/kcms/time/package/contents/ui/Digit.qml new file mode 100644 index 00000000..cfdf63ac --- /dev/null +++ b/kcms/time/package/contents/ui/Digit.qml @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.7 +import org.kde.kirigami 2.4 as Kirigami + + +Item { + id: root + + property alias model: spinnerView.model + property alias currentIndex: spinnerView.currentIndex + property alias delegate: spinnerView.delegate + property alias moving: spinnerView.moving + property int selectedIndex: -1 + property int fontSize: 14 + + width: placeHolder.width*1.3 + height: placeHolder.height*3 + + Text { + id: placeHolder + visible: false + font.pointSize: root.fontSize + text: "00" + } + + PathView { + id: spinnerView + anchors.fill: parent + model: 60 + clip: true + pathItemCount: 5 + dragMargin: 800 + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + delegate: Text { + horizontalAlignment: Text.AlignHCenter + width: spinnerView.width + property int ownIndex: index + text: index < 10 ? "0"+index : index + color: Kirigami.Theme.textColor + font.pointSize: root.fontSize + opacity: PathView.itemOpacity + } + + onMovingChanged: { + userConfiguring = true + if (!moving) { + userConfiguringTimer.restart() + selectedIndex = childAt(width/2, height/2).ownIndex + } + } + + path: Path { + startX: spinnerView.width/2 + startY: spinnerView.height + 1.5*placeHolder.height + PathAttribute { name: "itemOpacity"; value: 0 } + PathLine { + x: spinnerView.width/2 + y: spinnerView.height/2 + } + PathAttribute { name: "itemOpacity"; value: 1 } + PathLine { + x: spinnerView.width/2 + y: -1.5*placeHolder.height + } + PathAttribute { name: "itemOpacity"; value: 0 } + } + } +} + diff --git a/kcms/time/package/contents/ui/TimePicker.qml b/kcms/time/package/contents/ui/TimePicker.qml new file mode 100644 index 00000000..86c255fb --- /dev/null +++ b/kcms/time/package/contents/ui/TimePicker.qml @@ -0,0 +1,216 @@ +/* + SPDX-FileCopyrightText: 2011 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.1 + +import org.kde.kirigami 2.4 as Kirigami + + +//FIXME: shouldn't be a FrameSvgItem +Item { + id: root + clip: true + + //////// API + property alias hours: clockRow.hours + property alias minutes: clockRow.minutes + property alias seconds: clockRow.seconds + + property bool userConfiguring: false + property bool twentyFour: true + + property int fontSize: 14 + property int _margin: Kirigami.Units.gridUnit + + property string timeString: clockRow.twoDigitString(hours) + ":" + clockRow.twoDigitString(minutes) + ":" + clockRow.twoDigitString(seconds) + + opacity: enabled ? 1.0 : 0.5 + + Connections { + target: root +// onHoursChanged: print("H : " + root.hours) +// onMinutesChanged: print("M : " + root.minutes) +// onSecondsChanged: print("S : " + root.seconds) + } + + Behavior on width { + SequentialAnimation { + PauseAnimation { + duration: 250 + } + NumberAnimation { + //duration: PlasmaCore.Units.longDuration + duration: 250 + easing.type: Easing.InOutQuad + } + } + } + +// KLocale.Locale { +// id: locale +// } + + //imagePath: "widgets/picker" + width: clockRow.width + root._margin + height: clockRow.height + root._margin * 2 + + Timer { + id: userConfiguringTimer + repeat: false + interval: 1500 + running: false + onTriggered: { + root.hours = clockRow.hours + root.minutes = clockRow.minutes + root.seconds = clockRow.seconds + userConfiguring = false + } + } + + Rectangle { + color: "transparent" + opacity: 0.3 + border.color: Kirigami.Theme.textColor + border.width: 1 + anchors.fill: parent + } + + Row { + id: clockRow + spacing: Kirigami.Units.gridUnit + x: root._margin + y: root._margin + + property int hours + property int minutes + property int seconds + + function twoDigitString(number) + { + return number < 10 ? "0"+number : number + } + + Digit { + id: hoursDigit + model: root.twentyFour ? 24 : 12 + currentIndex: root.twentyFour || hours < 12 ? hours : hours - 12 + delegate: Text { + horizontalAlignment: Text.AlignHCenter + width: hoursDigit.width + property int ownIndex: index + text: (!root.twentyFour && index == 0) ? "12" : clockRow.twoDigitString(index) + font.pointSize: root.fontSize + color: Kirigami.Theme.textColor + opacity: PathView.itemOpacity + } + onSelectedIndexChanged: { + print("Bah"); + if (selectedIndex > -1) { + if (root.twentyFour || + meridiaeDigit.isAm) { + hours = selectedIndex + } else { + hours = selectedIndex + 12 + } + } + } + } + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + } + } + Digit { + id: minutesDigit + model: 60 + currentIndex: minutes + onSelectedIndexChanged: { + if (selectedIndex > -1) { + minutes = selectedIndex + } + } + } + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + } + } + Digit { + id: secondsDigit + model: 60 + currentIndex: seconds + onSelectedIndexChanged: { + if (selectedIndex > -1) { + seconds = selectedIndex + } + } + } + Kirigami.Separator { + opacity: meridiaeDigit.opacity == 0 ? 0 : 1 + + anchors { + top: parent.top + bottom: parent.bottom + } + Behavior on opacity { + NumberAnimation { + duration: 250 + easing.type: Easing.InOutQuad + } + } + } + Digit { + id: meridiaeDigit + visible: opacity != 0 + opacity: root.twentyFour ? 0 : 1 + property bool isAm: (selectedIndex > -1) ? (selectedIndex < 1) : (currentIndex < 1) + model: ListModel { + ListElement { + meridiae: "AM" + } + ListElement { + meridiae: "PM" + } + } + delegate: Text { + width: meridiaeDigit.width + horizontalAlignment: Text.AlignLeft + property int ownIndex: index + text: meridiae + color: Kirigami.Theme.textColor + font.pointSize: root.fontSize + //opacity: PathView.itemOpacity + } + currentIndex: hours > 12 ? 1 : 0 + onSelectedIndexChanged: { + if (selectedIndex > -1) { + //AM + if (selectedIndex == 0) { + hours -= 12 + //PM + } else { + hours += 12 + } + } + } + width: meridiaePlaceHolder.width + root._margin + Text { + id: meridiaePlaceHolder + visible: false + font.pointSize: root.fontSize + text: "00" + } + Behavior on opacity { + NumberAnimation { + duration: 250 + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/kcms/time/package/contents/ui/main.qml b/kcms/time/package/contents/ui/main.qml new file mode 100644 index 00000000..ce3f3d03 --- /dev/null +++ b/kcms/time/package/contents/ui/main.qml @@ -0,0 +1,257 @@ +// -*- coding: iso-8859-1 -*- +/* + * SPDX-FileCopyrightText: 2011 Sebastian Kügler + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +import QtQuick 2.7 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.3 as Controls + +import org.kde.kirigami 2.10 as Kirigami +import org.kde.kcm 1.2 +import org.kde.timesettings 1.0 +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +SimpleKCM { + id: timeModule + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + Component { + id: listDelegateComponent + + Kirigami.BasicListItem { + text: { + if (model) { + if (model.region) { + return "%1 / %2".arg(model.region).arg(model.city) + } else { + return model.city + } + } + return "" + } + onClicked: { + timeZonePickerSheet.close() + kcm.saveTimeZone(model.timeZoneId) + } + } + } + + ColumnLayout { + spacing: 0 + width: parent.width + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Display") + } + + MobileForm.FormSwitchDelegate { + id: hourFormatSwitch + text: i18n("24-Hour Format") + description: i18n("Whether to use a 24-hour format for clocks.") + checked: kcm.twentyFour + onCheckedChanged: { + kcm.twentyFour = checked + } + } + + MobileForm.FormDelegateSeparator { above: hourFormatSwitch; below: timeZoneSelect } + + MobileForm.FormButtonDelegate { + id: timeZoneSelect + text: i18n("Timezone") + description: kcm.timeZone + onClicked: timeZonePickerSheet.open() + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: i18n("Time and Date") + } + + MobileForm.FormSwitchDelegate { + id: ntpCheckBox + text: i18n("Automatic Time Synchronization") + description: i18n("Whether to set the time automatically.") + checked: kcm.useNtp + onCheckedChanged: { + kcm.useNtp = checked + if (!checked) { + kcm.ntpServer = "" + kcm.saveTime() + } + } + } + + MobileForm.FormDelegateSeparator { above: ntpCheckBox; below: timeSelect } + + MobileForm.FormButtonDelegate { + id: timeSelect + enabled: !ntpCheckBox.checked + icon.name: "clock" + text: i18n("Current Time") + description: Qt.formatTime(kcm.currentTime, Locale.LongFormat) + onClicked: timePickerSheet.open() + } + + MobileForm.FormDelegateSeparator { above: timeSelect; below: dateSelect } + + MobileForm.FormButtonDelegate { + id: dateSelect + enabled: !ntpCheckBox.checked + icon.name: "view-calendar" + text: i18n("Date") + description: Qt.formatDate(kcm.currentDate, Locale.LongFormat) + onClicked: datePickerSheet.open() + } + } + } + } + + Kirigami.OverlaySheet { + id: timeZonePickerSheet + header: ColumnLayout { + Kirigami.Heading { + text: i18nc("@title:window", "Pick Timezone") + } + Kirigami.SearchField { + Layout.fillWidth: true + width: parent.width + onTextChanged: { + kcm.timeZonesModel.filterString = text + } + } + } + + footer: RowLayout { + Controls.Button { + Layout.alignment: Qt.AlignHCenter + + text: i18nc("@action:button", "Close") + + onClicked: timeZonePickerSheet.close() + } + } + ListView { + clip: true + anchors.fill: parent + implicitWidth: 18 * Kirigami.Units.gridUnit + model: kcm.timeZonesModel + delegate: Kirigami.DelegateRecycler { + width: parent.width + + sourceComponent: listDelegateComponent + } + } + } + + Kirigami.OverlaySheet { + id: timePickerSheet + header: Kirigami.Heading { text: i18n("Pick Time") } + TimePicker { + id: timePicker + enabled: !ntpCheckBox.checked + twentyFour: twentyFourSwitch.checked + + implicitWidth: width > Kirigami.Units.gridUnit * 15 ? width : Kirigami.Units.gridUnit * 15 + + Component.onCompleted: { + var date = new Date(kcm.currentTime); + timePicker.hours = date.getHours(); + timePicker.minutes = date.getMinutes(); + timePicker.seconds = date.getSeconds(); + } + Connections { + target: kcm + onCurrentTimeChanged: { + if (timePicker.userConfiguring) { + return; + } + + var date = new Date(kcm.currentTime); + timePicker.hours = date.getHours(); + timePicker.minutes = date.getMinutes(); + timePicker.seconds = date.getSeconds(); + } + } + onUserConfiguringChanged: { + kcm.currentTime = timeString + kcm.saveTime() + } + } + footer: RowLayout { + Controls.Button { + Layout.alignment: Qt.AlignRight + + text: i18nc("@action:button", "Close") + + onClicked: timePickerSheet.close() + } + } + } + + Kirigami.OverlaySheet { + id: datePickerSheet + header: Kirigami.Heading { text: i18n("Pick Date") } + DatePicker { + id: datePicker + enabled: !ntpCheckBox.checked + + implicitWidth: width > Kirigami.Units.gridUnit * 15 ? width : Kirigami.Units.gridUnit * 15 + + Component.onCompleted: { + var date = new Date(kcm.currentDate) + datePicker.day = date.getDate() + datePicker.month = date.getMonth()+1 + datePicker.year = date.getFullYear() + } + Connections { + target: kcm + onCurrentDateChanged: { + if (datePicker.userConfiguring) { + return + } + + var date = new Date(kcm.currentDate) + + datePicker.day = date.getDate() + datePicker.month = date.getMonth()+1 + datePicker.year = date.getFullYear() + } + } + onUserConfiguringChanged: { + kcm.currentDate = isoDate + kcm.saveTime() + } + } + footer: RowLayout { + Controls.Button { + Layout.alignment: Qt.AlignRight + + text: i18nc("@action:button", "Close") + + onClicked: datePickerSheet.close() + } + } + } +} diff --git a/kcms/time/timedated1.xml b/kcms/time/timedated1.xml new file mode 100644 index 00000000..7555d5e5 --- /dev/null +++ b/kcms/time/timedated1.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kcms/time/timesettings.cpp b/kcms/time/timesettings.cpp new file mode 100644 index 00000000..58d29a84 --- /dev/null +++ b/kcms/time/timesettings.cpp @@ -0,0 +1,278 @@ +/* + + SPDX-FileCopyrightText: 2005 S.R.Haque . + SPDX-FileCopyrightText: 2009 David Faure + SPDX-FileCopyrightText: 2011-2015 Sebastian Kügler + SPDX-FileCopyrightText: 2015 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "timesettings.h" +#include "timezonemodel.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "timedated_interface.h" + +#define FORMAT24H "HH:mm:ss" +#define FORMAT12H "h:mm:ss ap" + +K_PLUGIN_FACTORY_WITH_JSON(TimeSettingsFactory, "timesettings.json", registerPlugin();) + +TimeSettings::TimeSettings(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, metaData, args) + , m_useNtp(true) +{ + qDebug() << "time settings init"; + m_timeZonesModel = nullptr; + setTimeZone(QTimeZone::systemTimeZone().id()); + + setButtons(Apply | Default); + + qmlRegisterAnonymousType("org.kde.timesettings", 1); + qmlRegisterAnonymousType("org.kde.timesettings", 1); + + initSettings(); + initTimeZones(); + qDebug() << "TimeSettings module loaded."; +} + +TimeSettings::~TimeSettings() +{ +} + +void TimeSettings::initTimeZones() +{ + auto *filterModel = new TimeZoneFilterProxy(this); + filterModel->setSourceModel(new TimeZoneModel(filterModel)); + setTimeZonesModel(filterModel); +} + +void TimeSettings::initSettings() +{ + m_localeConfig = KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig); + m_localeSettings = KConfigGroup(m_localeConfig, "Locale"); + + setTimeFormat(m_localeSettings.readEntry("TimeFormat", QStringLiteral(FORMAT24H))); // FIXME?! + + OrgFreedesktopTimedate1Interface timeDatedIface(QStringLiteral("org.freedesktop.timedate1"), + QStringLiteral("/org/freedesktop/timedate1"), + QDBusConnection::systemBus()); + // the server list is not relevant for timesyncd, it fetches it from the network + m_useNtp = timeDatedIface.nTP(); +} + +void TimeSettings::timeout() +{ + setCurrentTime(QTime::currentTime()); + setCurrentDate(QDate::currentDate()); + notify(); +} + +QString TimeSettings::currentTimeText() +{ + return m_currentTimeText; +} + +QTime TimeSettings::currentTime() const +{ + return m_currentTime; +} + +void TimeSettings::setCurrentTime(const QTime ¤tTime) +{ + if (m_currentTime != currentTime) { + m_currentTime = currentTime; + m_currentTimeText = QLocale().toString(QTime::currentTime(), m_timeFormat); + emit currentTimeChanged(); + } +} + +QDate TimeSettings::currentDate() const +{ + return m_currentDate; +} + +void TimeSettings::setCurrentDate(const QDate ¤tDate) +{ + if (m_currentDate != currentDate) { + m_currentDate = currentDate; + emit currentDateChanged(); + } +} + +bool TimeSettings::useNtp() const +{ + return m_useNtp; +} + +void TimeSettings::setUseNtp(bool ntp) +{ + if (m_useNtp != ntp) { + m_useNtp = ntp; + saveTime(); + emit useNtpChanged(); + } +} + +bool TimeSettings::saveTime() +{ + OrgFreedesktopTimedate1Interface timedateIface(QStringLiteral("org.freedesktop.timedate1"), + QStringLiteral("/org/freedesktop/timedate1"), + QDBusConnection::systemBus()); + + bool rc = true; + // final arg in each method is "user-interaction" i.e whether it's OK for polkit to ask for auth + + // we cannot send requests up front then block for all replies as we need NTP to be disabled before we can make a call to SetTime + // timedated processes these in parallel and will return an error otherwise + + auto reply = timedateIface.SetNTP(m_useNtp, true); + reply.waitForFinished(); + if (reply.isError()) { + m_errorString = i18n("Unable to change NTP settings"); + emit errorStringChanged(); + qWarning() << "Failed to enable NTP" << reply.error().name() << reply.error().message(); + rc = false; + } + + if (!useNtp()) { + QDateTime userTime; + userTime.setTime(currentTime()); + userTime.setDate(currentDate()); + qDebug() << "Setting userTime: " << userTime; + qint64 timeDiff = userTime.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(); + //*1000 for milliseconds -> microseconds + auto reply = timedateIface.SetTime(timeDiff * 1000, true, true); + reply.waitForFinished(); + if (reply.isError()) { + m_errorString = i18n("Unable to set current time"); + emit errorStringChanged(); + qWarning() << "Failed to set current time" << reply.error().name() << reply.error().message(); + rc = false; + } + } + saveTimeZone(m_timezone); + + return rc; +} + +void TimeSettings::saveTimeZone(const QString &newtimezone) +{ + qDebug() << "Saving timezone to config: " << newtimezone; + OrgFreedesktopTimedate1Interface timedateIface(QStringLiteral("org.freedesktop.timedate1"), + QStringLiteral("/org/freedesktop/timedate1"), + QDBusConnection::systemBus()); + + if (!newtimezone.isEmpty()) { + qDebug() << "Setting timezone: " << newtimezone; + auto reply = timedateIface.SetTimezone(newtimezone, true); + reply.waitForFinished(); + if (reply.isError()) { + m_errorString = i18n("Unable to set timezone"); + emit errorStringChanged(); + qWarning() << "Failed to set timezone" << reply.error().name() << reply.error().message(); + } + } + + setTimeZone(newtimezone); + emit timeZoneChanged(); + notify(); +} + +QString TimeSettings::timeFormat() +{ + return m_timeFormat; +} + +void TimeSettings::setTimeFormat(const QString &timeFormat) +{ + if (m_timeFormat != timeFormat) { + m_timeFormat = timeFormat; + + m_localeSettings.writeEntry("TimeFormat", timeFormat, KConfigGroup::Notify); + m_localeConfig->sync(); + + QDBusMessage msg = + QDBusMessage::createSignal(QStringLiteral("/org/kde/kcmshell_clock"), QStringLiteral("org.kde.kcmshell_clock"), QStringLiteral("clockUpdated")); + QDBusConnection::sessionBus().send(msg); + + qDebug() << "time format is now: " << QLocale().toString(QTime::currentTime(), m_timeFormat); + emit timeFormatChanged(); + timeout(); + } +} + +QString TimeSettings::timeZone() +{ + return m_timezone; +} + +void TimeSettings::setTimeZone(const QString &timezone) +{ + if (m_timezone != timezone) { + m_timezone = timezone; + qDebug() << "timezone changed to: " << timezone; + emit timeZoneChanged(); + timeout(); + } +} + +TimeZoneFilterProxy *TimeSettings::timeZonesModel() +{ + return m_timeZonesModel; +} + +void TimeSettings::setTimeZonesModel(TimeZoneFilterProxy *timezones) +{ + m_timeZonesModel = timezones; + emit timeZonesModelChanged(); +} + +bool TimeSettings::twentyFour() +{ + return timeFormat() == QStringLiteral(FORMAT24H); +} + +void TimeSettings::setTwentyFour(bool t) +{ + if (twentyFour() != t) { + if (t) { + setTimeFormat(FORMAT24H); + } else { + setTimeFormat(FORMAT12H); + } + qDebug() << "T24 toggled: " << t << m_timeFormat; + emit twentyFourChanged(); + emit currentTimeChanged(); + timeout(); + } +} + +QString TimeSettings::errorString() +{ + return m_errorString; +} + +void TimeSettings::notify() +{ + const QDBusMessage msg = + QDBusMessage::createSignal(QStringLiteral("/org/kde/kcmshell_clock"), QStringLiteral("org.kde.kcmshell_clock"), QStringLiteral("clockUpdated")); + QDBusConnection::sessionBus().send(msg); +} + +#include "timesettings.moc" diff --git a/kcms/time/timesettings.h b/kcms/time/timesettings.h new file mode 100644 index 00000000..254bf19f --- /dev/null +++ b/kcms/time/timesettings.h @@ -0,0 +1,120 @@ +/* + + SPDX-FileCopyrightText: 2011-2015 Sebastian Kügler + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TIMESETTINGS_H +#define TIMESETTINGS_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "timezonemodel.h" + +// #include "settingsmodule.h" + +class TimeSettingsPrivate; + +/** + * @class TimeSettings A class to manage time and date related settings. This class serves two functions: + * - Provide a plugin implementation + * - Provide a settings module + * This is done from one class in order to simplify the code. You can export any QObject-based + * class through qmlRegisterType(), however. + */ +class TimeSettings : public KQuickAddons::ConfigModule +{ + Q_OBJECT + + Q_PROPERTY(QString timeFormat READ timeFormat WRITE setTimeFormat NOTIFY timeFormatChanged) + Q_PROPERTY(bool twentyFour READ twentyFour WRITE setTwentyFour NOTIFY twentyFourChanged) + Q_PROPERTY(QString timeZone READ timeZone WRITE setTimeZone NOTIFY timeZoneChanged) + Q_PROPERTY(TimeZoneFilterProxy *timeZonesModel READ timeZonesModel WRITE setTimeZonesModel NOTIFY timeZonesModelChanged) + Q_PROPERTY(QTime currentTime READ currentTime WRITE setCurrentTime NOTIFY currentTimeChanged) + Q_PROPERTY(QDate currentDate READ currentDate WRITE setCurrentDate NOTIFY currentDateChanged) + Q_PROPERTY(bool useNtp READ useNtp WRITE setUseNtp NOTIFY useNtpChanged) + Q_PROPERTY(QString currentTimeText READ currentTimeText NOTIFY currentTimeTextChanged) + Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged) + +public: + /** + * @name Settings Module Constructor + * + * @arg parent The parent object + * @arg list Arguments, currently unused + */ + TimeSettings(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args); + ~TimeSettings() override; + + QString currentTimeText(); + QTime currentTime() const; + void setCurrentTime(const QTime &time); + + QDate currentDate() const; + void setCurrentDate(const QDate &date); + + bool useNtp() const; + void setUseNtp(bool ntp); + + QString timeFormat(); + QString timeZone(); + TimeZoneFilterProxy *timeZonesModel(); + bool twentyFour(); + + QString errorString(); + +public Q_SLOTS: + void setTimeZone(const QString &timezone); + void setTimeZonesModel(TimeZoneFilterProxy *timezones); + void setTimeFormat(const QString &timeFormat); + void setTwentyFour(bool t); + void timeout(); + bool saveTime(); + void notify(); + Q_INVOKABLE void saveTimeZone(const QString &newtimezone); + +Q_SIGNALS: + void currentTimeTextChanged(); + void currentTimeChanged(); + void currentDateChanged(); + void twentyFourChanged(); + void timeFormatChanged(); + void timeZoneChanged(); + void timeZonesChanged(); + void timeZonesModelChanged(); + void useNtpChanged(); + void errorStringChanged(); + +protected: + QString findNtpUtility(); + +private: + QString m_timeFormat; + QString m_timezone; + TimeZoneFilterProxy *m_timeZonesModel; + QString m_timeZoneFilter; + QString m_currentTimeText; + QTime m_currentTime; + QDate m_currentDate; + bool m_useNtp; + QString m_errorString; + + void initSettings(); + void initTimeZones(); + + KSharedConfig::Ptr m_localeConfig; + KConfigGroup m_localeSettings; +}; + +#endif // TIMESETTINGS_H diff --git a/kcms/time/timesettings.json b/kcms/time/timesettings.json new file mode 100644 index 00000000..7f8d8354 --- /dev/null +++ b/kcms/time/timesettings.json @@ -0,0 +1,128 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-system;", + "KPlugin": { + "Description": "Timezone, Date Display", + "Description[az]": "Saat Qurşağı, Tarix göstərişi", + "Description[ca@valencia]": "Zona horària, Mostra la data", + "Description[ca]": "Zona horària, Mostra la data", + "Description[cs]": "Zobrazení data a časového pásma", + "Description[de]": "Zeitzone, Datumsanzeige", + "Description[en_GB]": "Timezone, Date Display", + "Description[es]": "Zona horaria, visor de la fecha", + "Description[eu]": "Ordu-eremua, data azaltzea", + "Description[fi]": "Aikavyöhyke, Päivämääränäyttö", + "Description[fr]": "Fuseau horaire, affichage de la date", + "Description[hu]": "Időzóna, dátummegjelenítés", + "Description[ia]": "Fuso horari, Monstrator de Data", + "Description[id]": "Zona Waktu, Tampilan Tanggal", + "Description[is]": "Tímabelti, birting dagsetninga", + "Description[it]": "Fuso orario, visualizzazione della data", + "Description[ka]": "დროის სარტყელი, თარიღის ჩვენება", + "Description[ko]": "시간대, 날짜 표시", + "Description[lt]": "Laiko juosta, datos atvaizdavimas", + "Description[nl]": "Tijdzone, datumweergave", + "Description[nn]": "Tidssone, datovising", + "Description[pa]": "ਸਮਾਂ-ਖੇਤਰ, ਤਾਰੀਖ ਦਿਖਾਓ", + "Description[pl]": "Strefa czasowa, wyświetlanie daty", + "Description[pt]": "Fuso-Horário, Visualização das Datas", + "Description[pt_BR]": "Fuso horário, exibição de datas", + "Description[ro]": "Fus orar, afișare dată", + "Description[ru]": "Часовой пояс, формат вывода даты", + "Description[sl]": "Časovni pas, prikaz datuma", + "Description[sv]": "Tidszon, datumvisning", + "Description[tr]": "Zaman Dilimi, Tarih Gösterimi", + "Description[uk]": "Часовий пояс, показ дати", + "Description[vi]": "Hiển thị múi giờ, ngày", + "Description[x-test]": "xxTimezone, Date Displayxx", + "Description[zh_CN]": "时区、日期显示", + "Description[zh_TW]": "時區、日期顯示", + "FormFactors": [ + "handset", + "tablet", + "mediacenter" + ], + "Icon": "preferences-system-time", + "Name": "Time and Date", + "Name[az]": "Vaxt və Tarixi ayarlamaq", + "Name[ca@valencia]": "Hora i data", + "Name[ca]": "Hora i data", + "Name[cs]": "Datum a čas", + "Name[da]": "Klokkeslæt og dato", + "Name[de]": "Zeit und Datum", + "Name[el]": "Ώρα και ημερομηνία", + "Name[en_GB]": "Time and Date", + "Name[es]": "Fecha y hora", + "Name[et]": "Kellaaeg ja kuupäev", + "Name[eu]": "Ordua eta data", + "Name[fi]": "Aika ja päivämäärä", + "Name[fr]": "Heure et Date", + "Name[gl]": "Hora e data", + "Name[hu]": "Dátum és idő", + "Name[ia]": "Hora e data", + "Name[id]": "Waktu dan Tanggal", + "Name[is]": "Dagsetning og tími", + "Name[it]": "Ora e data", + "Name[ka]": "თარიღი და დრო", + "Name[ko]": "날짜와 시간", + "Name[lt]": "Laikas ir data", + "Name[nl]": "Datum en tijd", + "Name[nn]": "Dato og klokkeslett", + "Name[pa]": "ਸਮਾਂ ਅਤੇ ਮਿਤੀ", + "Name[pl]": "Czas i data", + "Name[pt]": "Hora e Data", + "Name[pt_BR]": "Data e hora", + "Name[ro]": "Oră și dată", + "Name[ru]": "Дата и время", + "Name[sk]": "Čas a dátum", + "Name[sl]": "Čas in datum", + "Name[sv]": "Tid och datum", + "Name[ta]": "நேரமும் தேதியும்", + "Name[tr]": "Zaman ve Tarih", + "Name[uk]": "Час і дата", + "Name[vi]": "Ngày và giờ", + "Name[x-test]": "xxTime and Datexx", + "Name[zh_CN]": "时间和日期", + "Name[zh_TW]": "日期與時間" + }, + "X-KDE-Keywords": "theme, look, feel", + "X-KDE-Keywords[ast]": "estilu, aspeutu", + "X-KDE-Keywords[az]": "theme, look, feel,mövzu,xarici görünüş,hissetmə", + "X-KDE-Keywords[ca@valencia]": "tema, aspecte, comportament", + "X-KDE-Keywords[ca]": "tema, aspecte, comportament", + "X-KDE-Keywords[cs]": "motiv,vzhled,chování", + "X-KDE-Keywords[da]": "tema, udseende, fremtoning", + "X-KDE-Keywords[de]": "Design,Erscheinungsbild", + "X-KDE-Keywords[el]": "θέμα, όψη, αίσθηση", + "X-KDE-Keywords[en_GB]": "theme, look, feel", + "X-KDE-Keywords[es]": "tema, aspecto, visual", + "X-KDE-Keywords[et]": "teema, välimus", + "X-KDE-Keywords[eu]": "gaia,itxura,izaera", + "X-KDE-Keywords[fi]": "teema, ulkoasu, tuntuma", + "X-KDE-Keywords[fr]": "thème, apparence, comportement, graphique", + "X-KDE-Keywords[gl]": "tema, aparencia, estilo, comportamento", + "X-KDE-Keywords[hu]": "téma, kinézet, megjelenés", + "X-KDE-Keywords[ia]": "thema, semblantia, apparentia", + "X-KDE-Keywords[it]": "tema, aspetto", + "X-KDE-Keywords[ko]": "theme, look, feel, 테마, 모습과 느낌, 외형, 외관", + "X-KDE-Keywords[lt]": "apipavidalinimas, išvaizda, isvaizda, turinys", + "X-KDE-Keywords[nl]": "thema, uiterlijk, gedrag", + "X-KDE-Keywords[nn]": "tema, utsjånad, åtferd", + "X-KDE-Keywords[pa]": "ਥੀਮ, ਦਿੱਖ, ਰਵੱਈਆ", + "X-KDE-Keywords[pl]": "wystrój, wygląd, odczucia, wrażenia", + "X-KDE-Keywords[pt]": "tema, aparência, comportamento", + "X-KDE-Keywords[pt_BR]": "tema, visual, aparência", + "X-KDE-Keywords[ro]": "tematică, temă, aspect, comportament", + "X-KDE-Keywords[ru]": "theme,look,feel,тема,оформление,внешний вид,стиль", + "X-KDE-Keywords[sk]": "téma, vzhľad, nastavenie", + "X-KDE-Keywords[sl]": "tema, videz, občutek", + "X-KDE-Keywords[sv]": "tema, utseende, känsla", + "X-KDE-Keywords[ta]": "theme, look, feel, தோற்றம், தோற்றத்திட்டம், பார்வை, காட்சி, உணர்வு", + "X-KDE-Keywords[tr]": "tema, bak, hisset", + "X-KDE-Keywords[uk]": "theme,look,feel,тема,вигляд,поведінка", + "X-KDE-Keywords[vi]": "theme,look,feel,chủ đề,nhìn,cảm", + "X-KDE-Keywords[x-test]": "xxthemexx,xx lookxx,xx feelxx", + "X-KDE-Keywords[zh_CN]": "theme, look, feel, 主题, 外观, 视觉效果, 显示效果", + "X-KDE-Keywords[zh_TW]": "theme, look, feel, 主題, 外觀, 感覺", + "X-KDE-System-Settings-Parent-Category": "regionalsettings", + "X-KDE-Weight": 70 +} diff --git a/kcms/time/timezonedata.h b/kcms/time/timezonedata.h new file mode 100644 index 00000000..98d1338b --- /dev/null +++ b/kcms/time/timezonedata.h @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TIMEZONEDATA_H +#define TIMEZONEDATA_H + +#include + +class TimeZoneData +{ +public: + QString id; + QString region; + QString city; + QString comment; + bool checked; + int offsetFromUtc; +}; + +#endif // TIMEZONEDATA_H diff --git a/kcms/time/timezonemodel.cpp b/kcms/time/timezonemodel.cpp new file mode 100644 index 00000000..4960eb82 --- /dev/null +++ b/kcms/time/timezonemodel.cpp @@ -0,0 +1,217 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "timezonemodel.h" +#include "timezonesi18n.h" + +#include +#include +#include + +TimeZoneFilterProxy::TimeZoneFilterProxy(QObject *parent) + : QSortFilterProxyModel(parent) +{ + m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive); +} + +bool TimeZoneFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!sourceModel() || m_filterString.isEmpty()) { + return true; + } + + const QString city = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CityRole).toString(); + const QString region = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::RegionRole).toString(); + const QString comment = sourceModel()->index(source_row, 0, source_parent).data(TimeZoneModel::CommentRole).toString(); + + if (m_stringMatcher.indexIn(city) != -1 || m_stringMatcher.indexIn(region) != -1 || m_stringMatcher.indexIn(comment) != -1) { + return true; + } + + return false; +} + +void TimeZoneFilterProxy::setFilterString(const QString &filterString) +{ + m_filterString = filterString; + m_stringMatcher.setPattern(filterString); + emit filterStringChanged(); + invalidateFilter(); +} + +//============================================================================= + +TimeZoneModel::TimeZoneModel(QObject *parent) + : QAbstractListModel(parent) + , m_timezonesI18n(new TimezonesI18n(this)) +{ + update(); +} + +TimeZoneModel::~TimeZoneModel() +{ +} + +int TimeZoneModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_data.count(); +} + +QVariant TimeZoneModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid()) { + TimeZoneData currentData = m_data.at(index.row()); + + switch (role) { + case TimeZoneIdRole: + return currentData.id; + case RegionRole: + return currentData.region; + case CityRole: + return currentData.city; + case CommentRole: + return currentData.comment; + case CheckedRole: + return currentData.checked; + } + } + + return QVariant(); +} + +bool TimeZoneModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || value.isNull()) { + return false; + } + + if (role == CheckedRole) { + m_data[index.row()].checked = value.toBool(); + emit dataChanged(index, index); + + if (m_data[index.row()].checked) { + m_selectedTimeZones.append(m_data[index.row()].id); + m_offsetData.insert(m_data[index.row()].id, m_data[index.row()].offsetFromUtc); + } else { + m_selectedTimeZones.removeAll(m_data[index.row()].id); + m_offsetData.remove(m_data[index.row()].id); + } + + sortTimeZones(); + + emit selectedTimeZonesChanged(); + return true; + } + + return false; +} + +void TimeZoneModel::update() +{ + beginResetModel(); + m_data.clear(); + + QTimeZone localZone = QTimeZone(QTimeZone::systemTimeZoneId()); + const QStringList data = QString::fromUtf8(localZone.id()).split(QLatin1Char('/')); + + TimeZoneData local; + local.id = QStringLiteral("Local"); + local.region = i18nc("This means \"Local Timezone\"", "Local"); + local.city = m_timezonesI18n->i18nCity(data.last()); + local.comment = i18n("Your system time zone"); + local.checked = false; + + m_data.append(local); + + QStringList cities; + QHash zonesByCity; + + const QList systemTimeZones = QTimeZone::availableTimeZoneIds(); + + for (auto it = systemTimeZones.constBegin(); it != systemTimeZones.constEnd(); ++it) { + const QTimeZone zone(*it); + const QStringList splitted = QString::fromUtf8(zone.id()).split(QStringLiteral("/")); + + // CITY | COUNTRY | CONTINENT + const QString key = QStringLiteral("%1|%2|%3").arg(splitted.last(), QLocale::countryToString(zone.country()), splitted.first()); + + cities.append(key); + zonesByCity.insert(key, zone); + } + cities.sort(Qt::CaseInsensitive); + + Q_FOREACH (const QString &key, cities) { + const QTimeZone timeZone = zonesByCity.value(key); + QString comment = timeZone.comment(); + + // FIXME - this was in the old code but makes no sense + // if (!comment.isEmpty()) { + // comment = i18n(comment.toUtf8()); + // } + + const QStringList cityCountryContinent = key.split(QLatin1Char('|')); + + TimeZoneData newData; + newData.id = QString::fromUtf8(timeZone.id()); + newData.region = timeZone.country() == QLocale::AnyCountry + ? QString() + : m_timezonesI18n->i18nContinents(cityCountryContinent.at(2)) + QLatin1Char('/') + m_timezonesI18n->i18nCountry(timeZone.country()); + newData.city = m_timezonesI18n->i18nCity(cityCountryContinent.at(0)); + newData.comment = comment; + newData.checked = false; + newData.offsetFromUtc = timeZone.offsetFromUtc(QDateTime::currentDateTimeUtc()); + m_data.append(newData); + } + + endResetModel(); +} + +void TimeZoneModel::setSelectedTimeZones(const QStringList &selectedTimeZones) +{ + m_selectedTimeZones = selectedTimeZones; + for (int i = 0; i < m_data.size(); i++) { + if (m_selectedTimeZones.contains(m_data.at(i).id)) { + m_data[i].checked = true; + m_offsetData.insert(m_data[i].id, m_data[i].offsetFromUtc); + + QModelIndex index = createIndex(i, 0); + emit dataChanged(index, index); + } + } + + sortTimeZones(); +} + +void TimeZoneModel::selectLocalTimeZone() +{ + m_data[0].checked = true; + + QModelIndex index = createIndex(0, 0); + emit dataChanged(index, index); + + m_selectedTimeZones << m_data[0].id; + emit selectedTimeZonesChanged(); +} + +QHash TimeZoneModel::roleNames() const +{ + return QHash({ + {TimeZoneIdRole, "timeZoneId"}, + {RegionRole, "region"}, + {CityRole, "city"}, + {CommentRole, "comment"}, + {CheckedRole, "checked"}, + }); +} + +void TimeZoneModel::sortTimeZones() +{ + std::sort(m_selectedTimeZones.begin(), m_selectedTimeZones.end(), [this](const QString &a, const QString &b) { + return m_offsetData.value(a) < m_offsetData.value(b); + }); +} diff --git a/kcms/time/timezonemodel.h b/kcms/time/timezonemodel.h new file mode 100644 index 00000000..1bc71078 --- /dev/null +++ b/kcms/time/timezonemodel.h @@ -0,0 +1,80 @@ +/* + SPDX-FileCopyrightText: 2014 Kai Uwe Broulik + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef TIMEZONEMODEL_H +#define TIMEZONEMODEL_H + +#include +#include + +#include "timezonedata.h" + +class TimezonesI18n; + +class TimeZoneFilterProxy : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filterString WRITE setFilterString MEMBER m_filterString NOTIFY filterStringChanged) + +public: + explicit TimeZoneFilterProxy(QObject *parent = nullptr); + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + + void setFilterString(const QString &filterString); + +Q_SIGNALS: + void filterStringChanged(); + +private: + QString m_filterString; + QStringMatcher m_stringMatcher; +}; + +//============================================================================= + +class TimeZoneModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QStringList selectedTimeZones WRITE setSelectedTimeZones MEMBER m_selectedTimeZones NOTIFY selectedTimeZonesChanged) + +public: + explicit TimeZoneModel(QObject *parent = nullptr); + ~TimeZoneModel() override; + + enum Roles { + TimeZoneIdRole = Qt::UserRole + 1, + RegionRole, + CityRole, + CommentRole, + CheckedRole, + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + void update(); + void setSelectedTimeZones(const QStringList &selectedTimeZones); + + Q_INVOKABLE void selectLocalTimeZone(); + +Q_SIGNALS: + void selectedTimeZonesChanged(); + +protected: + QHash roleNames() const override; + +private: + void sortTimeZones(); + + QList m_data; + QHash m_offsetData; // used for sorting + QStringList m_selectedTimeZones; + TimezonesI18n *m_timezonesI18n; +}; + +#endif // TIMEZONEMODEL_H diff --git a/kcms/time/timezonesi18n.cpp b/kcms/time/timezonesi18n.cpp new file mode 100644 index 00000000..7de2b516 --- /dev/null +++ b/kcms/time/timezonesi18n.cpp @@ -0,0 +1,787 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#include "timezonesi18n.h" + +#include + +TimezonesI18n::TimezonesI18n(QObject *parent) + : QObject(parent) + , m_isInitialized(false) +{ +} + +QString TimezonesI18n::i18nCity(const QString &city) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nCities.value(city); +} + +QString TimezonesI18n::i18nContinents(const QString &continent) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nContinents.value(continent); +} + +QString TimezonesI18n::i18nCountry(QLocale::Country country) +{ + if (!m_isInitialized) { + init(); + } + return m_i18nCountries.value(country); +} + +void TimezonesI18n::init() +{ + m_i18nCities = + QHash({{QStringLiteral("Abidjan"), i18nc("This is a city associated with particular time zone", "Abidjan")}, + {QStringLiteral("Accra"), i18nc("This is a city associated with particular time zone", "Accra")}, + {QStringLiteral("Adak"), i18nc("This is a city associated with particular time zone", "Adak")}, + {QStringLiteral("Addis_Ababa"), i18nc("This is a city associated with particular time zone", "Addis Ababa")}, + {QStringLiteral("Adelaide"), i18nc("This is a city associated with particular time zone", "Adelaide")}, + {QStringLiteral("Aden"), i18nc("This is a city associated with particular time zone", "Aden")}, + {QStringLiteral("Algiers"), i18nc("This is a city associated with particular time zone", "Algiers")}, + {QStringLiteral("Almaty"), i18nc("This is a city associated with particular time zone", "Almaty")}, + {QStringLiteral("Amman"), i18nc("This is a city associated with particular time zone", "Amman")}, + {QStringLiteral("Amsterdam"), i18nc("This is a city associated with particular time zone", "Amsterdam")}, + {QStringLiteral("Anadyr"), i18nc("This is a city associated with particular time zone", "Anadyr")}, + {QStringLiteral("Anchorage"), i18nc("This is a city associated with particular time zone", "Anchorage")}, + {QStringLiteral("Andorra"), i18nc("This is a city associated with particular time zone", "Andorra")}, + {QStringLiteral("Anguilla"), i18nc("This is a city associated with particular time zone", "Anguilla")}, + {QStringLiteral("Antananarivo"), i18nc("This is a city associated with particular time zone", "Antananarivo")}, + {QStringLiteral("Antigua"), i18nc("This is a city associated with particular time zone", "Antigua")}, + {QStringLiteral("Apia"), i18nc("This is a city associated with particular time zone", "Apia")}, + {QStringLiteral("Aqtau"), i18nc("This is a city associated with particular time zone", "Aqtau")}, + {QStringLiteral("Aqtobe"), i18nc("This is a city associated with particular time zone", "Aqtobe")}, + {QStringLiteral("Araguaina"), i18nc("This is a city associated with particular time zone", "Araguaina")}, + {QStringLiteral("Aruba"), i18nc("This is a city associated with particular time zone", "Aruba")}, + {QStringLiteral("Ashgabat"), i18nc("This is a city associated with particular time zone", "Ashgabat")}, + {QStringLiteral("Asmara"), i18nc("This is a city associated with particular time zone", "Asmara")}, + {QStringLiteral("Astrakhan"), i18nc("This is a city associated with particular time zone", "Astrakhan")}, + {QStringLiteral("Asuncion"), i18nc("This is a city associated with particular time zone", "Asuncion")}, + {QStringLiteral("Athens"), i18nc("This is a city associated with particular time zone", "Athens")}, + {QStringLiteral("Atikokan"), i18nc("This is a city associated with particular time zone", "Atikokan")}, + {QStringLiteral("Atyrau"), i18nc("This is a city associated with particular time zone", "Atyrau")}, + {QStringLiteral("Auckland"), i18nc("This is a city associated with particular time zone", "Auckland")}, + {QStringLiteral("Azores"), i18nc("This is a city associated with particular time zone", "Azores")}, + {QStringLiteral("Baghdad"), i18nc("This is a city associated with particular time zone", "Baghdad")}, + {QStringLiteral("Bahia_Banderas"), i18nc("This is a city associated with particular time zone", "Bahia Banderas")}, + {QStringLiteral("Bahia"), i18nc("This is a city associated with particular time zone", "Bahia")}, + {QStringLiteral("Bahrain"), i18nc("This is a city associated with particular time zone", "Bahrain")}, + {QStringLiteral("Baku"), i18nc("This is a city associated with particular time zone", "Baku")}, + {QStringLiteral("Bamako"), i18nc("This is a city associated with particular time zone", "Bamako")}, + {QStringLiteral("Bangkok"), i18nc("This is a city associated with particular time zone", "Bangkok")}, + {QStringLiteral("Bangui"), i18nc("This is a city associated with particular time zone", "Bangui")}, + {QStringLiteral("Banjul"), i18nc("This is a city associated with particular time zone", "Banjul")}, + {QStringLiteral("Barbados"), i18nc("This is a city associated with particular time zone", "Barbados")}, + {QStringLiteral("Barnaul"), i18nc("This is a city associated with particular time zone", "Barnaul")}, + {QStringLiteral("Beirut"), i18nc("This is a city associated with particular time zone", "Beirut")}, + {QStringLiteral("Belem"), i18nc("This is a city associated with particular time zone", "Belem")}, + {QStringLiteral("Belgrade"), i18nc("This is a city associated with particular time zone", "Belgrade")}, + {QStringLiteral("Belize"), i18nc("This is a city associated with particular time zone", "Belize")}, + {QStringLiteral("Berlin"), i18nc("This is a city associated with particular time zone", "Berlin")}, + {QStringLiteral("Bermuda"), i18nc("This is a city associated with particular time zone", "Bermuda")}, + {QStringLiteral("Beulah"), i18nc("This is a city associated with particular time zone", "Beulah")}, + {QStringLiteral("Bishkek"), i18nc("This is a city associated with particular time zone", "Bishkek")}, + {QStringLiteral("Bissau"), i18nc("This is a city associated with particular time zone", "Bissau")}, + {QStringLiteral("Blanc-Sablon"), i18nc("This is a city associated with particular time zone", "Blanc-Sablon")}, + {QStringLiteral("Blantyre"), i18nc("This is a city associated with particular time zone", "Blantyre")}, + {QStringLiteral("Boa_Vista"), i18nc("This is a city associated with particular time zone", "Boa Vista")}, + {QStringLiteral("Bogota"), i18nc("This is a city associated with particular time zone", "Bogota")}, + {QStringLiteral("Boise"), i18nc("This is a city associated with particular time zone", "Boise")}, + {QStringLiteral("Bougainville"), i18nc("This is a city associated with particular time zone", "Bougainville")}, + {QStringLiteral("Bratislava"), i18nc("This is a city associated with particular time zone", "Bratislava")}, + {QStringLiteral("Brazzaville"), i18nc("This is a city associated with particular time zone", "Brazzaville")}, + {QStringLiteral("Brisbane"), i18nc("This is a city associated with particular time zone", "Brisbane")}, + {QStringLiteral("Broken_Hill"), i18nc("This is a city associated with particular time zone", "Broken Hill")}, + {QStringLiteral("Brunei"), i18nc("This is a city associated with particular time zone", "Brunei")}, + {QStringLiteral("Brussels"), i18nc("This is a city associated with particular time zone", "Brussels")}, + {QStringLiteral("Bucharest"), i18nc("This is a city associated with particular time zone", "Bucharest")}, + {QStringLiteral("Budapest"), i18nc("This is a city associated with particular time zone", "Budapest")}, + {QStringLiteral("Buenos_Aires"), i18nc("This is a city associated with particular time zone", "Buenos Aires")}, + {QStringLiteral("Bujumbura"), i18nc("This is a city associated with particular time zone", "Bujumbura")}, + {QStringLiteral("Busingen"), i18nc("This is a city associated with particular time zone", "Busingen")}, + {QStringLiteral("Cairo"), i18nc("This is a city associated with particular time zone", "Cairo")}, + {QStringLiteral("Cambridge_Bay"), i18nc("This is a city associated with particular time zone", "Cambridge Bay")}, + {QStringLiteral("Campo_Grande"), i18nc("This is a city associated with particular time zone", "Campo Grande")}, + {QStringLiteral("Canary"), i18nc("This is a city associated with particular time zone", "Canary")}, + {QStringLiteral("Cancun"), i18nc("This is a city associated with particular time zone", "Cancun")}, + {QStringLiteral("Cape_Verde"), i18nc("This is a city associated with particular time zone", "Cape Verde")}, + {QStringLiteral("Caracas"), i18nc("This is a city associated with particular time zone", "Caracas")}, + {QStringLiteral("Casablanca"), i18nc("This is a city associated with particular time zone", "Casablanca")}, + {QStringLiteral("Casey"), i18nc("This is a city associated with particular time zone", "Casey")}, + {QStringLiteral("Catamarca"), i18nc("This is a city associated with particular time zone", "Catamarca")}, + {QStringLiteral("Cayenne"), i18nc("This is a city associated with particular time zone", "Cayenne")}, + {QStringLiteral("Cayman"), i18nc("This is a city associated with particular time zone", "Cayman")}, + {QStringLiteral("Center"), i18nc("This is a city associated with particular time zone", "Center")}, + {QStringLiteral("Ceuta"), i18nc("This is a city associated with particular time zone", "Ceuta")}, + {QStringLiteral("Chagos"), i18nc("This is a city associated with particular time zone", "Chagos")}, + {QStringLiteral("Chatham"), i18nc("This is a city associated with particular time zone", "Chatham")}, + {QStringLiteral("Chicago"), i18nc("This is a city associated with particular time zone", "Chicago")}, + {QStringLiteral("Chihuahua"), i18nc("This is a city associated with particular time zone", "Chihuahua")}, + {QStringLiteral("Chisinau"), i18nc("This is a city associated with particular time zone", "Chisinau")}, + {QStringLiteral("Chita"), i18nc("This is a city associated with particular time zone", "Chita")}, + {QStringLiteral("Choibalsan"), i18nc("This is a city associated with particular time zone", "Choibalsan")}, + {QStringLiteral("Chongqing"), i18nc("This is a city associated with particular time zone", "Chongqing")}, + {QStringLiteral("Christmas"), i18nc("This is a city associated with particular time zone", "Christmas")}, + {QStringLiteral("Chuuk"), i18nc("This is a city associated with particular time zone", "Chuuk")}, + {QStringLiteral("Cocos"), i18nc("This is a city associated with particular time zone", "Cocos")}, + {QStringLiteral("Colombo"), i18nc("This is a city associated with particular time zone", "Colombo")}, + {QStringLiteral("Comoro"), i18nc("This is a city associated with particular time zone", "Comoro")}, + {QStringLiteral("Conakry"), i18nc("This is a city associated with particular time zone", "Conakry")}, + {QStringLiteral("Copenhagen"), i18nc("This is a city associated with particular time zone", "Copenhagen")}, + {QStringLiteral("Cordoba"), i18nc("This is a city associated with particular time zone", "Cordoba")}, + {QStringLiteral("Costa_Rica"), i18nc("This is a city associated with particular time zone", "Costa Rica")}, + {QStringLiteral("Creston"), i18nc("This is a city associated with particular time zone", "Creston")}, + {QStringLiteral("Cuiaba"), i18nc("This is a city associated with particular time zone", "Cuiaba")}, + {QStringLiteral("Curacao"), i18nc("This is a city associated with particular time zone", "Curacao")}, + {QStringLiteral("Currie"), i18nc("This is a city associated with particular time zone", "Currie")}, + {QStringLiteral("Dakar"), i18nc("This is a city associated with particular time zone", "Dakar")}, + {QStringLiteral("Damascus"), i18nc("This is a city associated with particular time zone", "Damascus")}, + {QStringLiteral("Danmarkshavn"), i18nc("This is a city associated with particular time zone", "Danmarkshavn")}, + {QStringLiteral("Dar_es_Salaam"), i18nc("This is a city associated with particular time zone", "Dar es Salaam")}, + {QStringLiteral("Darwin"), i18nc("This is a city associated with particular time zone", "Darwin")}, + {QStringLiteral("Davis"), i18nc("This is a city associated with particular time zone", "Davis")}, + {QStringLiteral("Dawson_Creek"), i18nc("This is a city associated with particular time zone", "Dawson Creek")}, + {QStringLiteral("Dawson"), i18nc("This is a city associated with particular time zone", "Dawson")}, + {QStringLiteral("Denver"), i18nc("This is a city associated with particular time zone", "Denver")}, + {QStringLiteral("Detroit"), i18nc("This is a city associated with particular time zone", "Detroit")}, + {QStringLiteral("Dhaka"), i18nc("This is a city associated with particular time zone", "Dhaka")}, + {QStringLiteral("Dili"), i18nc("This is a city associated with particular time zone", "Dili")}, + {QStringLiteral("Djibouti"), i18nc("This is a city associated with particular time zone", "Djibouti")}, + {QStringLiteral("Dominica"), i18nc("This is a city associated with particular time zone", "Dominica")}, + {QStringLiteral("Douala"), i18nc("This is a city associated with particular time zone", "Douala")}, + {QStringLiteral("Dubai"), i18nc("This is a city associated with particular time zone", "Dubai")}, + {QStringLiteral("Dublin"), i18nc("This is a city associated with particular time zone", "Dublin")}, + {QStringLiteral("DumontDUrville"), i18nc("This is a city associated with particular time zone", "Dumont d’Urville")}, + {QStringLiteral("Dushanbe"), i18nc("This is a city associated with particular time zone", "Dushanbe")}, + {QStringLiteral("Easter"), i18nc("This is a city associated with particular time zone", "Easter")}, + {QStringLiteral("Edmonton"), i18nc("This is a city associated with particular time zone", "Edmonton")}, + {QStringLiteral("Efate"), i18nc("This is a city associated with particular time zone", "Efate")}, + {QStringLiteral("Eirunepe"), i18nc("This is a city associated with particular time zone", "Eirunepe")}, + {QStringLiteral("El_Aaiun"), i18nc("This is a city associated with particular time zone", "El Aaiun")}, + {QStringLiteral("El_Salvador"), i18nc("This is a city associated with particular time zone", "El Salvador")}, + {QStringLiteral("Enderbury"), i18nc("This is a city associated with particular time zone", "Enderbury")}, + {QStringLiteral("Eucla"), i18nc("This is a city associated with particular time zone", "Eucla")}, + {QStringLiteral("Fakaofo"), i18nc("This is a city associated with particular time zone", "Fakaofo")}, + {QStringLiteral("Famagusta"), i18nc("This is a city associated with particular time zone", "Famagusta")}, + {QStringLiteral("Faroe"), i18nc("This is a city associated with particular time zone", "Faroe")}, + {QStringLiteral("Fiji"), i18nc("This is a city associated with particular time zone", "Fiji")}, + {QStringLiteral("Fort_Nelson"), i18nc("This is a city associated with particular time zone", "Fort Nelson")}, + {QStringLiteral("Fortaleza"), i18nc("This is a city associated with particular time zone", "Fortaleza")}, + {QStringLiteral("Freetown"), i18nc("This is a city associated with particular time zone", "Freetown")}, + {QStringLiteral("Funafuti"), i18nc("This is a city associated with particular time zone", "Funafuti")}, + {QStringLiteral("Gaborone"), i18nc("This is a city associated with particular time zone", "Gaborone")}, + {QStringLiteral("Galapagos"), i18nc("This is a city associated with particular time zone", "Galapagos")}, + {QStringLiteral("Gambier"), i18nc("This is a city associated with particular time zone", "Gambier")}, + {QStringLiteral("Gaza"), i18nc("This is a city associated with particular time zone", "Gaza")}, + {QStringLiteral("Gibraltar"), i18nc("This is a city associated with particular time zone", "Gibraltar")}, + {QStringLiteral("Glace_Bay"), i18nc("This is a city associated with particular time zone", "Glace Bay")}, + {QStringLiteral("Godthab"), i18nc("This is a city associated with particular time zone", "Godthab")}, + {QStringLiteral("Goose_Bay"), i18nc("This is a city associated with particular time zone", "Goose Bay")}, + {QStringLiteral("Grand_Turk"), i18nc("This is a city associated with particular time zone", "Grand Turk")}, + {QStringLiteral("Grenada"), i18nc("This is a city associated with particular time zone", "Grenada")}, + {QStringLiteral("Guadalcanal"), i18nc("This is a city associated with particular time zone", "Guadalcanal")}, + {QStringLiteral("Guadeloupe"), i18nc("This is a city associated with particular time zone", "Guadeloupe")}, + {QStringLiteral("Guam"), i18nc("This is a city associated with particular time zone", "Guam")}, + {QStringLiteral("Guatemala"), i18nc("This is a city associated with particular time zone", "Guatemala")}, + {QStringLiteral("Guayaquil"), i18nc("This is a city associated with particular time zone", "Guayaquil")}, + {QStringLiteral("Guernsey"), i18nc("This is a city associated with particular time zone", "Guernsey")}, + {QStringLiteral("Guyana"), i18nc("This is a city associated with particular time zone", "Guyana")}, + {QStringLiteral("Halifax"), i18nc("This is a city associated with particular time zone", "Halifax")}, + {QStringLiteral("Harare"), i18nc("This is a city associated with particular time zone", "Harare")}, + {QStringLiteral("Harbin"), i18nc("This is a city associated with particular time zone", "Harbin")}, + {QStringLiteral("Havana"), i18nc("This is a city associated with particular time zone", "Havana")}, + {QStringLiteral("Hebron"), i18nc("This is a city associated with particular time zone", "Hebron")}, + {QStringLiteral("Helsinki"), i18nc("This is a city associated with particular time zone", "Helsinki")}, + {QStringLiteral("Hermosillo"), i18nc("This is a city associated with particular time zone", "Hermosillo")}, + {QStringLiteral("Ho_Chi_Minh"), i18nc("This is a city associated with particular time zone", "Ho Chi Minh")}, + {QStringLiteral("Hobart"), i18nc("This is a city associated with particular time zone", "Hobart")}, + {QStringLiteral("Hong_Kong"), i18nc("This is a city associated with particular time zone", "Hong Kong")}, + {QStringLiteral("Honolulu"), i18nc("This is a city associated with particular time zone", "Honolulu")}, + {QStringLiteral("Hovd"), i18nc("This is a city associated with particular time zone", "Hovd")}, + {QStringLiteral("Indianapolis"), i18nc("This is a city associated with particular time zone", "Indianapolis")}, + {QStringLiteral("Inuvik"), i18nc("This is a city associated with particular time zone", "Inuvik")}, + {QStringLiteral("Iqaluit"), i18nc("This is a city associated with particular time zone", "Iqaluit")}, + {QStringLiteral("Irkutsk"), i18nc("This is a city associated with particular time zone", "Irkutsk")}, + {QStringLiteral("Isle_of_Man"), i18nc("This is a city associated with particular time zone", "Isle of Man")}, + {QStringLiteral("Istanbul"), i18nc("This is a city associated with particular time zone", "Istanbul")}, + {QStringLiteral("Jakarta"), i18nc("This is a city associated with particular time zone", "Jakarta")}, + {QStringLiteral("Jamaica"), i18nc("This is a city associated with particular time zone", "Jamaica")}, + {QStringLiteral("Jayapura"), i18nc("This is a city associated with particular time zone", "Jayapura")}, + {QStringLiteral("Jersey"), i18nc("This is a city associated with particular time zone", "Jersey")}, + {QStringLiteral("Jerusalem"), i18nc("This is a city associated with particular time zone", "Jerusalem")}, + {QStringLiteral("Johannesburg"), i18nc("This is a city associated with particular time zone", "Johannesburg")}, + {QStringLiteral("Johnston"), i18nc("This is a city associated with particular time zone", "Johnston")}, + {QStringLiteral("Juba"), i18nc("This is a city associated with particular time zone", "Juba")}, + {QStringLiteral("Jujuy"), i18nc("This is a city associated with particular time zone", "Jujuy")}, + {QStringLiteral("Juneau"), i18nc("This is a city associated with particular time zone", "Juneau")}, + {QStringLiteral("Kabul"), i18nc("This is a city associated with particular time zone", "Kabul")}, + {QStringLiteral("Kaliningrad"), i18nc("This is a city associated with particular time zone", "Kaliningrad")}, + {QStringLiteral("Kamchatka"), i18nc("This is a city associated with particular time zone", "Kamchatka")}, + {QStringLiteral("Kampala"), i18nc("This is a city associated with particular time zone", "Kampala")}, + {QStringLiteral("Karachi"), i18nc("This is a city associated with particular time zone", "Karachi")}, + {QStringLiteral("Kashgar"), i18nc("This is a city associated with particular time zone", "Kashgar")}, + {QStringLiteral("Kathmandu"), i18nc("This is a city associated with particular time zone", "Kathmandu")}, + {QStringLiteral("Kerguelen"), i18nc("This is a city associated with particular time zone", "Kerguelen")}, + {QStringLiteral("Khandyga"), i18nc("This is a city associated with particular time zone", "Khandyga")}, + {QStringLiteral("Khartoum"), i18nc("This is a city associated with particular time zone", "Khartoum")}, + {QStringLiteral("Kiev"), i18nc("This is a city associated with particular time zone", "Kyiv")}, + {QStringLiteral("Kigali"), i18nc("This is a city associated with particular time zone", "Kigali")}, + {QStringLiteral("Kinshasa"), i18nc("This is a city associated with particular time zone", "Kinshasa")}, + {QStringLiteral("Kiritimati"), i18nc("This is a city associated with particular time zone", "Kiritimati")}, + {QStringLiteral("Kirov"), i18nc("This is a city associated with particular time zone", "Kirov")}, + {QStringLiteral("Knox"), i18nc("This is a city associated with particular time zone", "Knox")}, + {QStringLiteral("Kolkata"), i18nc("This is a city associated with particular time zone", "Kolkata")}, + {QStringLiteral("Kosrae"), i18nc("This is a city associated with particular time zone", "Kosrae")}, + {QStringLiteral("Kralendijk"), i18nc("This is a city associated with particular time zone", "Kralendijk")}, + {QStringLiteral("Krasnoyarsk"), i18nc("This is a city associated with particular time zone", "Krasnoyarsk")}, + {QStringLiteral("Kuala_Lumpur"), i18nc("This is a city associated with particular time zone", "Kuala Lumpur")}, + {QStringLiteral("Kuching"), i18nc("This is a city associated with particular time zone", "Kuching")}, + {QStringLiteral("Kuwait"), i18nc("This is a city associated with particular time zone", "Kuwait")}, + {QStringLiteral("Kwajalein"), i18nc("This is a city associated with particular time zone", "Kwajalein")}, + {QStringLiteral("La_Paz"), i18nc("This is a city associated with particular time zone", "La Paz")}, + {QStringLiteral("La_Rioja"), i18nc("This is a city associated with particular time zone", "La Rioja")}, + {QStringLiteral("Lagos"), i18nc("This is a city associated with particular time zone", "Lagos")}, + {QStringLiteral("Libreville"), i18nc("This is a city associated with particular time zone", "Libreville")}, + {QStringLiteral("Lima"), i18nc("This is a city associated with particular time zone", "Lima")}, + {QStringLiteral("Lindeman"), i18nc("This is a city associated with particular time zone", "Lindeman")}, + {QStringLiteral("Lisbon"), i18nc("This is a city associated with particular time zone", "Lisbon")}, + {QStringLiteral("Ljubljana"), i18nc("This is a city associated with particular time zone", "Ljubljana")}, + {QStringLiteral("Lome"), i18nc("This is a city associated with particular time zone", "Lome")}, + {QStringLiteral("London"), i18nc("This is a city associated with particular time zone", "London")}, + {QStringLiteral("Longyearbyen"), i18nc("This is a city associated with particular time zone", "Longyearbyen")}, + {QStringLiteral("Lord_Howe"), i18nc("This is a city associated with particular time zone", "Lord Howe")}, + {QStringLiteral("Los_Angeles"), i18nc("This is a city associated with particular time zone", "Los Angeles")}, + {QStringLiteral("Louisville"), i18nc("This is a city associated with particular time zone", "Louisville")}, + {QStringLiteral("Lower_Princes"), i18nc("This is a city associated with particular time zone", "Lower Princes")}, + {QStringLiteral("Luanda"), i18nc("This is a city associated with particular time zone", "Luanda")}, + {QStringLiteral("Lubumbashi"), i18nc("This is a city associated with particular time zone", "Lubumbashi")}, + {QStringLiteral("Lusaka"), i18nc("This is a city associated with particular time zone", "Lusaka")}, + {QStringLiteral("Luxembourg"), i18nc("This is a city associated with particular time zone", "Luxembourg")}, + {QStringLiteral("Macau"), i18nc("This is a city associated with particular time zone", "Macau")}, + {QStringLiteral("Maceio"), i18nc("This is a city associated with particular time zone", "Maceio")}, + {QStringLiteral("Macquarie"), i18nc("This is a city associated with particular time zone", "Macquarie")}, + {QStringLiteral("Madeira"), i18nc("This is a city associated with particular time zone", "Madeira")}, + {QStringLiteral("Madrid"), i18nc("This is a city associated with particular time zone", "Madrid")}, + {QStringLiteral("Magadan"), i18nc("This is a city associated with particular time zone", "Magadan")}, + {QStringLiteral("Mahe"), i18nc("This is a city associated with particular time zone", "Mahe")}, + {QStringLiteral("Majuro"), i18nc("This is a city associated with particular time zone", "Majuro")}, + {QStringLiteral("Makassar"), i18nc("This is a city associated with particular time zone", "Makassar")}, + {QStringLiteral("Malabo"), i18nc("This is a city associated with particular time zone", "Malabo")}, + {QStringLiteral("Maldives"), i18nc("This is a city associated with particular time zone", "Maldives")}, + {QStringLiteral("Malta"), i18nc("This is a city associated with particular time zone", "Malta")}, + {QStringLiteral("Managua"), i18nc("This is a city associated with particular time zone", "Managua")}, + {QStringLiteral("Manaus"), i18nc("This is a city associated with particular time zone", "Manaus")}, + {QStringLiteral("Manila"), i18nc("This is a city associated with particular time zone", "Manila")}, + {QStringLiteral("Maputo"), i18nc("This is a city associated with particular time zone", "Maputo")}, + {QStringLiteral("Marengo"), i18nc("This is a city associated with particular time zone", "Marengo")}, + {QStringLiteral("Mariehamn"), i18nc("This is a city associated with particular time zone", "Mariehamn")}, + {QStringLiteral("Marigot"), i18nc("This is a city associated with particular time zone", "Marigot")}, + {QStringLiteral("Marquesas"), i18nc("This is a city associated with particular time zone", "Marquesas")}, + {QStringLiteral("Martinique"), i18nc("This is a city associated with particular time zone", "Martinique")}, + {QStringLiteral("Maseru"), i18nc("This is a city associated with particular time zone", "Maseru")}, + {QStringLiteral("Matamoros"), i18nc("This is a city associated with particular time zone", "Matamoros")}, + {QStringLiteral("Mauritius"), i18nc("This is a city associated with particular time zone", "Mauritius")}, + {QStringLiteral("Mawson"), i18nc("This is a city associated with particular time zone", "Mawson")}, + {QStringLiteral("Mayotte"), i18nc("This is a city associated with particular time zone", "Mayotte")}, + {QStringLiteral("Mazatlan"), i18nc("This is a city associated with particular time zone", "Mazatlan")}, + {QStringLiteral("Mbabane"), i18nc("This is a city associated with particular time zone", "Mbabane")}, + {QStringLiteral("McMurdo"), i18nc("This is a city associated with particular time zone", "McMurdo")}, + {QStringLiteral("Melbourne"), i18nc("This is a city associated with particular time zone", "Melbourne")}, + {QStringLiteral("Mendoza"), i18nc("This is a city associated with particular time zone", "Mendoza")}, + {QStringLiteral("Menominee"), i18nc("This is a city associated with particular time zone", "Menominee")}, + {QStringLiteral("Merida"), i18nc("This is a city associated with particular time zone", "Merida")}, + {QStringLiteral("Metlakatla"), i18nc("This is a city associated with particular time zone", "Metlakatla")}, + {QStringLiteral("Mexico_City"), i18nc("This is a city associated with particular time zone", "Mexico City")}, + {QStringLiteral("Midway"), i18nc("This is a city associated with particular time zone", "Midway")}, + {QStringLiteral("Minsk"), i18nc("This is a city associated with particular time zone", "Minsk")}, + {QStringLiteral("Miquelon"), i18nc("This is a city associated with particular time zone", "Miquelon")}, + {QStringLiteral("Mogadishu"), i18nc("This is a city associated with particular time zone", "Mogadishu")}, + {QStringLiteral("Monaco"), i18nc("This is a city associated with particular time zone", "Monaco")}, + {QStringLiteral("Moncton"), i18nc("This is a city associated with particular time zone", "Moncton")}, + {QStringLiteral("Monrovia"), i18nc("This is a city associated with particular time zone", "Monrovia")}, + {QStringLiteral("Monterrey"), i18nc("This is a city associated with particular time zone", "Monterrey")}, + {QStringLiteral("Montevideo"), i18nc("This is a city associated with particular time zone", "Montevideo")}, + {QStringLiteral("Monticello"), i18nc("This is a city associated with particular time zone", "Monticello")}, + {QStringLiteral("Montserrat"), i18nc("This is a city associated with particular time zone", "Montserrat")}, + {QStringLiteral("Moscow"), i18nc("This is a city associated with particular time zone", "Moscow")}, + {QStringLiteral("Muscat"), i18nc("This is a city associated with particular time zone", "Muscat")}, + {QStringLiteral("Nairobi"), i18nc("This is a city associated with particular time zone", "Nairobi")}, + {QStringLiteral("Nassau"), i18nc("This is a city associated with particular time zone", "Nassau")}, + {QStringLiteral("Nauru"), i18nc("This is a city associated with particular time zone", "Nauru")}, + {QStringLiteral("Ndjamena"), i18nc("This is a city associated with particular time zone", "Ndjamena")}, + {QStringLiteral("New_Salem"), i18nc("This is a city associated with particular time zone", "New Salem")}, + {QStringLiteral("New_York"), i18nc("This is a city associated with particular time zone", "New York")}, + {QStringLiteral("Niamey"), i18nc("This is a city associated with particular time zone", "Niamey")}, + {QStringLiteral("Nicosia"), i18nc("This is a city associated with particular time zone", "Nicosia")}, + {QStringLiteral("Nipigon"), i18nc("This is a city associated with particular time zone", "Nipigon")}, + {QStringLiteral("Niue"), i18nc("This is a city associated with particular time zone", "Niue")}, + {QStringLiteral("Nome"), i18nc("This is a city associated with particular time zone", "Nome")}, + {QStringLiteral("Norfolk"), i18nc("This is a city associated with particular time zone", "Norfolk")}, + {QStringLiteral("Noronha"), i18nc("This is a city associated with particular time zone", "Noronha")}, + {QStringLiteral("Nouakchott"), i18nc("This is a city associated with particular time zone", "Nouakchott")}, + {QStringLiteral("Noumea"), i18nc("This is a city associated with particular time zone", "Noumea")}, + {QStringLiteral("Novokuznetsk"), i18nc("This is a city associated with particular time zone", "Novokuznetsk")}, + {QStringLiteral("Novosibirsk"), i18nc("This is a city associated with particular time zone", "Novosibirsk")}, + {QStringLiteral("Nuuk"), i18nc("This is a city associated with particular time zone", "Nuuk")}, + {QStringLiteral("Ojinaga"), i18nc("This is a city associated with particular time zone", "Ojinaga")}, + {QStringLiteral("Omsk"), i18nc("This is a city associated with particular time zone", "Omsk")}, + {QStringLiteral("Oral"), i18nc("This is a city associated with particular time zone", "Oral")}, + {QStringLiteral("Oslo"), i18nc("This is a city associated with particular time zone", "Oslo")}, + {QStringLiteral("Ouagadougou"), i18nc("This is a city associated with particular time zone", "Ouagadougou")}, + {QStringLiteral("Pago_Pago"), i18nc("This is a city associated with particular time zone", "Pago Pago")}, + {QStringLiteral("Palau"), i18nc("This is a city associated with particular time zone", "Palau")}, + {QStringLiteral("Palmer"), i18nc("This is a city associated with particular time zone", "Palmer")}, + {QStringLiteral("Panama"), i18nc("This is a city associated with particular time zone", "Panama")}, + {QStringLiteral("Pangnirtung"), i18nc("This is a city associated with particular time zone", "Pangnirtung")}, + {QStringLiteral("Paramaribo"), i18nc("This is a city associated with particular time zone", "Paramaribo")}, + {QStringLiteral("Paris"), i18nc("This is a city associated with particular time zone", "Paris")}, + {QStringLiteral("Perth"), i18nc("This is a city associated with particular time zone", "Perth")}, + {QStringLiteral("Petersburg"), i18nc("This is a city associated with particular time zone", "Petersburg")}, + {QStringLiteral("Phnom_Penh"), i18nc("This is a city associated with particular time zone", "Phnom Penh")}, + {QStringLiteral("Phoenix"), i18nc("This is a city associated with particular time zone", "Phoenix")}, + {QStringLiteral("Pitcairn"), i18nc("This is a city associated with particular time zone", "Pitcairn")}, + {QStringLiteral("Podgorica"), i18nc("This is a city associated with particular time zone", "Podgorica")}, + {QStringLiteral("Pohnpei"), i18nc("This is a city associated with particular time zone", "Pohnpei")}, + {QStringLiteral("Pontianak"), i18nc("This is a city associated with particular time zone", "Pontianak")}, + {QStringLiteral("Port-au-Prince"), i18nc("This is a city associated with particular time zone", "Port-au-Prince")}, + {QStringLiteral("Port_Moresby"), i18nc("This is a city associated with particular time zone", "Port Moresby")}, + {QStringLiteral("Port_of_Spain"), i18nc("This is a city associated with particular time zone", "Port of Spain")}, + {QStringLiteral("Porto-Novo"), i18nc("This is a city associated with particular time zone", "Porto-Novo")}, + {QStringLiteral("Porto_Velho"), i18nc("This is a city associated with particular time zone", "Porto Velho")}, + {QStringLiteral("Prague"), i18nc("This is a city associated with particular time zone", "Prague")}, + {QStringLiteral("Puerto_Rico"), i18nc("This is a city associated with particular time zone", "Puerto Rico")}, + {QStringLiteral("Punta_Arenas"), i18nc("This is a city associated with particular time zone", "Punta Arenas")}, + {QStringLiteral("Pyongyang"), i18nc("This is a city associated with particular time zone", "Pyongyang")}, + {QStringLiteral("Qatar"), i18nc("This is a city associated with particular time zone", "Qatar")}, + {QStringLiteral("Qostanay"), i18nc("This is a city associated with particular time zone", "Qostanay")}, + {QStringLiteral("Qyzylorda"), i18nc("This is a city associated with particular time zone", "Qyzylorda")}, + {QStringLiteral("Rainy_River"), i18nc("This is a city associated with particular time zone", "Rainy River")}, + {QStringLiteral("Rangoon"), i18nc("This is a city associated with particular time zone", "Rangoon")}, + {QStringLiteral("Rankin_Inlet"), i18nc("This is a city associated with particular time zone", "Rankin Inlet")}, + {QStringLiteral("Rarotonga"), i18nc("This is a city associated with particular time zone", "Rarotonga")}, + {QStringLiteral("Recife"), i18nc("This is a city associated with particular time zone", "Recife")}, + {QStringLiteral("Regina"), i18nc("This is a city associated with particular time zone", "Regina")}, + {QStringLiteral("Resolute"), i18nc("This is a city associated with particular time zone", "Resolute")}, + {QStringLiteral("Reunion"), i18nc("This is a city associated with particular time zone", "Reunion")}, + {QStringLiteral("Reykjavik"), i18nc("This is a city associated with particular time zone", "Reykjavik")}, + {QStringLiteral("Riga"), i18nc("This is a city associated with particular time zone", "Riga")}, + {QStringLiteral("Rio_Branco"), i18nc("This is a city associated with particular time zone", "Rio Branco")}, + {QStringLiteral("Rio_Gallegos"), i18nc("This is a city associated with particular time zone", "Rio Gallegos")}, + {QStringLiteral("Riyadh"), i18nc("This is a city associated with particular time zone", "Riyadh")}, + {QStringLiteral("Rome"), i18nc("This is a city associated with particular time zone", "Rome")}, + {QStringLiteral("Rothera"), i18nc("This is a city associated with particular time zone", "Rothera")}, + {QStringLiteral("Saipan"), i18nc("This is a city associated with particular time zone", "Saipan")}, + {QStringLiteral("Sakhalin"), i18nc("This is a city associated with particular time zone", "Sakhalin")}, + {QStringLiteral("Salta"), i18nc("This is a city associated with particular time zone", "Salta")}, + {QStringLiteral("Samara"), i18nc("This is a city associated with particular time zone", "Samara")}, + {QStringLiteral("Samarkand"), i18nc("This is a city associated with particular time zone", "Samarkand")}, + {QStringLiteral("San_Juan"), i18nc("This is a city associated with particular time zone", "San Juan")}, + {QStringLiteral("San_Luis"), i18nc("This is a city associated with particular time zone", "San Luis")}, + {QStringLiteral("San_Marino"), i18nc("This is a city associated with particular time zone", "San Marino")}, + {QStringLiteral("Santa_Isabel"), i18nc("This is a city associated with particular time zone", "Santa Isabel")}, + {QStringLiteral("Santarem"), i18nc("This is a city associated with particular time zone", "Santarem")}, + {QStringLiteral("Santiago"), i18nc("This is a city associated with particular time zone", "Santiago")}, + {QStringLiteral("Santo_Domingo"), i18nc("This is a city associated with particular time zone", "Santo Domingo")}, + {QStringLiteral("Sao_Paulo"), i18nc("This is a city associated with particular time zone", "Sao Paulo")}, + {QStringLiteral("Sao_Tome"), i18nc("This is a city associated with particular time zone", "Sao Tome")}, + {QStringLiteral("Sarajevo"), i18nc("This is a city associated with particular time zone", "Sarajevo")}, + {QStringLiteral("Saratov"), i18nc("This is a city associated with particular time zone", "Saratov")}, + {QStringLiteral("Scoresbysund"), i18nc("This is a city associated with particular time zone", "Scoresbysund")}, + {QStringLiteral("Seoul"), i18nc("This is a city associated with particular time zone", "Seoul")}, + {QStringLiteral("Shanghai"), i18nc("This is a city associated with particular time zone", "Shanghai")}, + {QStringLiteral("Simferopol"), i18nc("This is a city associated with particular time zone", "Simferopol")}, + {QStringLiteral("Singapore"), i18nc("This is a city associated with particular time zone", "Singapore")}, + {QStringLiteral("Sitka"), i18nc("This is a city associated with particular time zone", "Sitka")}, + {QStringLiteral("Skopje"), i18nc("This is a city associated with particular time zone", "Skopje")}, + {QStringLiteral("Sofia"), i18nc("This is a city associated with particular time zone", "Sofia")}, + {QStringLiteral("South_Georgia"), i18nc("This is a city associated with particular time zone", "South Georgia")}, + {QStringLiteral("Srednekolymsk"), i18nc("This is a city associated with particular time zone", "Srednekolymsk")}, + {QStringLiteral("St_Barthelemy"), i18nc("This is a city associated with particular time zone", "St Barthelemy")}, + {QStringLiteral("St_Helena"), i18nc("This is a city associated with particular time zone", "St Helena")}, + {QStringLiteral("St_Johns"), i18nc("This is a city associated with particular time zone", "St Johns")}, + {QStringLiteral("St_Kitts"), i18nc("This is a city associated with particular time zone", "St Kitts")}, + {QStringLiteral("St_Lucia"), i18nc("This is a city associated with particular time zone", "St Lucia")}, + {QStringLiteral("St_Thomas"), i18nc("This is a city associated with particular time zone", "St Thomas")}, + {QStringLiteral("St_Vincent"), i18nc("This is a city associated with particular time zone", "St Vincent")}, + {QStringLiteral("Stanley"), i18nc("This is a city associated with particular time zone", "Stanley")}, + {QStringLiteral("Stockholm"), i18nc("This is a city associated with particular time zone", "Stockholm")}, + {QStringLiteral("Swift_Current"), i18nc("This is a city associated with particular time zone", "Swift Current")}, + {QStringLiteral("Sydney"), i18nc("This is a city associated with particular time zone", "Sydney")}, + {QStringLiteral("Syowa"), i18nc("This is a city associated with particular time zone", "Syowa")}, + {QStringLiteral("Tahiti"), i18nc("This is a city associated with particular time zone", "Tahiti")}, + {QStringLiteral("Taipei"), i18nc("This is a city associated with particular time zone", "Taipei")}, + {QStringLiteral("Tallinn"), i18nc("This is a city associated with particular time zone", "Tallinn")}, + {QStringLiteral("Tarawa"), i18nc("This is a city associated with particular time zone", "Tarawa")}, + {QStringLiteral("Tashkent"), i18nc("This is a city associated with particular time zone", "Tashkent")}, + {QStringLiteral("Tbilisi"), i18nc("This is a city associated with particular time zone", "Tbilisi")}, + {QStringLiteral("Tegucigalpa"), i18nc("This is a city associated with particular time zone", "Tegucigalpa")}, + {QStringLiteral("Tehran"), i18nc("This is a city associated with particular time zone", "Tehran")}, + {QStringLiteral("Tell_City"), i18nc("This is a city associated with particular time zone", "Tell City")}, + {QStringLiteral("Thimphu"), i18nc("This is a city associated with particular time zone", "Thimphu")}, + {QStringLiteral("Thule"), i18nc("This is a city associated with particular time zone", "Thule")}, + {QStringLiteral("Thunder_Bay"), i18nc("This is a city associated with particular time zone", "Thunder Bay")}, + {QStringLiteral("Tijuana"), i18nc("This is a city associated with particular time zone", "Tijuana")}, + {QStringLiteral("Tirane"), i18nc("This is a city associated with particular time zone", "Tirane")}, + {QStringLiteral("Tokyo"), i18nc("This is a city associated with particular time zone", "Tokyo")}, + {QStringLiteral("Tomsk"), i18nc("This is a city associated with particular time zone", "Tomsk")}, + {QStringLiteral("Tongatapu"), i18nc("This is a city associated with particular time zone", "Tongatapu")}, + {QStringLiteral("Toronto"), i18nc("This is a city associated with particular time zone", "Toronto")}, + {QStringLiteral("Tortola"), i18nc("This is a city associated with particular time zone", "Tortola")}, + {QStringLiteral("Tripoli"), i18nc("This is a city associated with particular time zone", "Tripoli")}, + {QStringLiteral("Troll"), i18nc("This is a city associated with particular time zone", "Troll")}, + {QStringLiteral("Tucuman"), i18nc("This is a city associated with particular time zone", "Tucuman")}, + {QStringLiteral("Tunis"), i18nc("This is a city associated with particular time zone", "Tunis")}, + {QStringLiteral("Ulaanbaatar"), i18nc("This is a city associated with particular time zone", "Ulaanbaatar")}, + {QStringLiteral("Ulyanovsk"), i18nc("This is a city associated with particular time zone", "Ulyanovsk")}, + {QStringLiteral("Urumqi"), i18nc("This is a city associated with particular time zone", "Urumqi")}, + {QStringLiteral("Ushuaia"), i18nc("This is a city associated with particular time zone", "Ushuaia")}, + {QStringLiteral("Ust-Nera"), i18nc("This is a city associated with particular time zone", "Ust-Nera")}, + {QStringLiteral("UTC+00:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+00:00")}, + {QStringLiteral("UTC+01:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+01:00")}, + {QStringLiteral("UTC+02:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+02:00")}, + {QStringLiteral("UTC+03:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+03:00")}, + {QStringLiteral("UTC+03:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+03:30")}, + {QStringLiteral("UTC+04:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+04:00")}, + {QStringLiteral("UTC+04:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+04:30")}, + {QStringLiteral("UTC+05:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:00")}, + {QStringLiteral("UTC+05:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:30")}, + {QStringLiteral("UTC+05:45"), i18nc("This is a generic time zone name, localize as needed", "UTC+05:45")}, + {QStringLiteral("UTC+06:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+06:00")}, + {QStringLiteral("UTC+06:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+06:30")}, + {QStringLiteral("UTC+07:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+07:00")}, + {QStringLiteral("UTC+08:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+08:00")}, + {QStringLiteral("UTC+09:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+09:00")}, + {QStringLiteral("UTC+09:30"), i18nc("This is a generic time zone name, localize as needed", "UTC+09:30")}, + {QStringLiteral("UTC+10:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+10:00")}, + {QStringLiteral("UTC+11:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+11:00")}, + {QStringLiteral("UTC+12:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+12:00")}, + {QStringLiteral("UTC+13:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+13:00")}, + {QStringLiteral("UTC+14:00"), i18nc("This is a generic time zone name, localize as needed", "UTC+14:00")}, + {QStringLiteral("UTC-00:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-00:00")}, + {QStringLiteral("UTC-01:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-01:00")}, + {QStringLiteral("UTC-02:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-02:00")}, + {QStringLiteral("UTC-03:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-03:00")}, + {QStringLiteral("UTC-03:30"), i18nc("This is a generic time zone name, localize as needed", "UTC-03:30")}, + {QStringLiteral("UTC-04:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-04:00")}, + {QStringLiteral("UTC-04:30"), i18nc("This is a generic time zone name, localize as needed", "UTC-04:30")}, + {QStringLiteral("UTC-05:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-05:00")}, + {QStringLiteral("UTC-06:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-06:00")}, + {QStringLiteral("UTC-07:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-07:00")}, + {QStringLiteral("UTC-08:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-08:00")}, + {QStringLiteral("UTC-09:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-09:00")}, + {QStringLiteral("UTC-10:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-10:00")}, + {QStringLiteral("UTC-11:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-11:00")}, + {QStringLiteral("UTC-12:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-12:00")}, + {QStringLiteral("UTC-13:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-13:00")}, + {QStringLiteral("UTC-14:00"), i18nc("This is a generic time zone name, localize as needed", "UTC-14:00")}, + {QStringLiteral("UTC"), i18nc("This is a generic time zone name, localize as needed", "UTC")}, + {QStringLiteral("Uzhgorod"), i18nc("This is a city associated with particular time zone", "Uzhhorod")}, + {QStringLiteral("Vaduz"), i18nc("This is a city associated with particular time zone", "Vaduz")}, + {QStringLiteral("Vancouver"), i18nc("This is a city associated with particular time zone", "Vancouver")}, + {QStringLiteral("Vatican"), i18nc("This is a city associated with particular time zone", "Vatican")}, + {QStringLiteral("Vevay"), i18nc("This is a city associated with particular time zone", "Vevay")}, + {QStringLiteral("Vienna"), i18nc("This is a city associated with particular time zone", "Vienna")}, + {QStringLiteral("Vientiane"), i18nc("This is a city associated with particular time zone", "Vientiane")}, + {QStringLiteral("Vilnius"), i18nc("This is a city associated with particular time zone", "Vilnius")}, + {QStringLiteral("Vincennes"), i18nc("This is a city associated with particular time zone", "Vincennes")}, + {QStringLiteral("Vladivostok"), i18nc("This is a city associated with particular time zone", "Vladivostok")}, + {QStringLiteral("Volgograd"), i18nc("This is a city associated with particular time zone", "Volgograd")}, + {QStringLiteral("Vostok"), i18nc("This is a city associated with particular time zone", "Vostok")}, + {QStringLiteral("Wake"), i18nc("This is a city associated with particular time zone", "Wake")}, + {QStringLiteral("Wallis"), i18nc("This is a city associated with particular time zone", "Wallis")}, + {QStringLiteral("Warsaw"), i18nc("This is a city associated with particular time zone", "Warsaw")}, + {QStringLiteral("Whitehorse"), i18nc("This is a city associated with particular time zone", "Whitehorse")}, + {QStringLiteral("Winamac"), i18nc("This is a city associated with particular time zone", "Winamac")}, + {QStringLiteral("Windhoek"), i18nc("This is a city associated with particular time zone", "Windhoek")}, + {QStringLiteral("Winnipeg"), i18nc("This is a city associated with particular time zone", "Winnipeg")}, + {QStringLiteral("Yakutat"), i18nc("This is a city associated with particular time zone", "Yakutat")}, + {QStringLiteral("Yakutsk"), i18nc("This is a city associated with particular time zone", "Yakutsk")}, + {QStringLiteral("Yangon"), i18nc("This is a city associated with particular time zone", "Yangon")}, + {QStringLiteral("Yekaterinburg"), i18nc("This is a city associated with particular time zone", "Yekaterinburg")}, + {QStringLiteral("Yellowknife"), i18nc("This is a city associated with particular time zone", "Yellowknife")}, + {QStringLiteral("Yerevan"), i18nc("This is a city associated with particular time zone", "Yerevan")}, + {QStringLiteral("Zagreb"), i18nc("This is a city associated with particular time zone", "Zagreb")}, + {QStringLiteral("Zaporozhye"), i18nc("This is a city associated with particular time zone", "Zaporizhzhia")}, + {QStringLiteral("Zurich"), i18nc("This is a city associated with particular time zone", "Zurich")}}); + +#define ENTRY_ISO_3166(qlocale_enum, string) {QLocale::qlocale_enum, i18nd("iso_3166", string)} + /* Make sure the country names match their versions in iso-codes, + * ISO 3166. */ + m_i18nCountries = QHash({ + ENTRY_ISO_3166(IvoryCoast, "Côte d'Ivoire"), + ENTRY_ISO_3166(Ghana, "Ghana"), + ENTRY_ISO_3166(Ethiopia, "Ethiopia"), + ENTRY_ISO_3166(Algeria, "Algeria"), + ENTRY_ISO_3166(Eritrea, "Eritrea"), + ENTRY_ISO_3166(Mali, "Mali"), + ENTRY_ISO_3166(CentralAfricanRepublic, "Central African Republic"), + ENTRY_ISO_3166(Gambia, "Gambia"), + ENTRY_ISO_3166(GuineaBissau, "Guinea-Bissau"), + ENTRY_ISO_3166(Malawi, "Malawi"), + ENTRY_ISO_3166(CongoBrazzaville, "Congo"), + ENTRY_ISO_3166(Burundi, "Burundi"), + ENTRY_ISO_3166(Egypt, "Egypt"), + ENTRY_ISO_3166(Morocco, "Morocco"), + ENTRY_ISO_3166(Spain, "Spain"), + ENTRY_ISO_3166(Guinea, "Guinea"), + ENTRY_ISO_3166(Senegal, "Senegal"), + ENTRY_ISO_3166(Tanzania, "Tanzania"), + ENTRY_ISO_3166(Djibouti, "Djibouti"), + ENTRY_ISO_3166(Cameroon, "Cameroon"), + ENTRY_ISO_3166(WesternSahara, "Western Sahara"), + ENTRY_ISO_3166(SierraLeone, "Sierra Leone"), + ENTRY_ISO_3166(Botswana, "Botswana"), + ENTRY_ISO_3166(BouvetIsland, "Bouvet Island"), + ENTRY_ISO_3166(Zimbabwe, "Zimbabwe"), + ENTRY_ISO_3166(SouthAfrica, "South Africa"), + ENTRY_ISO_3166(SouthSudan, "South Sudan"), + ENTRY_ISO_3166(Uganda, "Uganda"), + ENTRY_ISO_3166(Sudan, "Sudan"), + ENTRY_ISO_3166(Rwanda, "Rwanda"), + ENTRY_ISO_3166(CongoKinshasa, "Congo, The Democratic Republic of the"), + ENTRY_ISO_3166(Nigeria, "Nigeria"), + ENTRY_ISO_3166(Gabon, "Gabon"), + ENTRY_ISO_3166(Togo, "Togo"), + ENTRY_ISO_3166(Angola, "Angola"), + ENTRY_ISO_3166(Zambia, "Zambia"), + ENTRY_ISO_3166(EquatorialGuinea, "Equatorial Guinea"), + ENTRY_ISO_3166(Mozambique, "Mozambique"), + ENTRY_ISO_3166(Lesotho, "Lesotho"), + ENTRY_ISO_3166(Swaziland, "Swaziland"), + ENTRY_ISO_3166(Somalia, "Somalia"), + ENTRY_ISO_3166(Liberia, "Liberia"), + ENTRY_ISO_3166(Kenya, "Kenya"), + ENTRY_ISO_3166(Chad, "Chad"), + ENTRY_ISO_3166(Niger, "Niger"), + ENTRY_ISO_3166(Mauritania, "Mauritania"), + ENTRY_ISO_3166(BurkinaFaso, "Burkina Faso"), + ENTRY_ISO_3166(Benin, "Benin"), + ENTRY_ISO_3166(SaoTomeAndPrincipe, "Sao Tome and Principe"), + ENTRY_ISO_3166(Libya, "Libya"), + ENTRY_ISO_3166(Tunisia, "Tunisia"), + ENTRY_ISO_3166(Namibia, "Namibia"), + ENTRY_ISO_3166(UnitedStates, "United States"), + ENTRY_ISO_3166(Anguilla, "Anguilla"), + ENTRY_ISO_3166(AntiguaAndBarbuda, "Antigua and Barbuda"), + ENTRY_ISO_3166(Brazil, "Brazil"), + ENTRY_ISO_3166(Argentina, "Argentina"), + ENTRY_ISO_3166(Aruba, "Aruba"), + ENTRY_ISO_3166(Paraguay, "Paraguay"), + ENTRY_ISO_3166(Canada, "Canada"), + ENTRY_ISO_3166(Mexico, "Mexico"), + ENTRY_ISO_3166(Barbados, "Barbados"), + ENTRY_ISO_3166(Belize, "Belize"), + ENTRY_ISO_3166(Colombia, "Colombia"), + ENTRY_ISO_3166(Venezuela, "Venezuela"), + ENTRY_ISO_3166(FrenchGuiana, "French Guiana"), + ENTRY_ISO_3166(CaymanIslands, "Cayman Islands"), + ENTRY_ISO_3166(CostaRica, "Costa Rica"), + ENTRY_ISO_3166(CuraSao, "Curaçao"), + ENTRY_ISO_3166(Greenland, "Greenland"), + ENTRY_ISO_3166(Dominica, "Dominica"), + ENTRY_ISO_3166(ElSalvador, "El Salvador"), + ENTRY_ISO_3166(TurksAndCaicosIslands, "Turks and Caicos Islands"), + ENTRY_ISO_3166(Grenada, "Grenada"), + ENTRY_ISO_3166(Guadeloupe, "Guadeloupe"), + ENTRY_ISO_3166(Guatemala, "Guatemala"), + ENTRY_ISO_3166(Ecuador, "Ecuador"), + ENTRY_ISO_3166(Guyana, "Guyana"), + ENTRY_ISO_3166(Cuba, "Cuba"), + ENTRY_ISO_3166(Jamaica, "Jamaica"), + ENTRY_ISO_3166(Bonaire, "Bonaire, Sint Eustatius and Saba"), + ENTRY_ISO_3166(Bolivia, "Bolivia"), + ENTRY_ISO_3166(Peru, "Peru"), + ENTRY_ISO_3166(SintMaarten, "Sint Maarten (Dutch part)"), + ENTRY_ISO_3166(Nicaragua, "Nicaragua"), + ENTRY_ISO_3166(SaintMartin, "Saint Martin (French part)"), + ENTRY_ISO_3166(Martinique, "Martinique"), + ENTRY_ISO_3166(SaintPierreAndMiquelon, "Saint Pierre and Miquelon"), + ENTRY_ISO_3166(Uruguay, "Uruguay"), + ENTRY_ISO_3166(Montserrat, "Montserrat"), + ENTRY_ISO_3166(Bahamas, "Bahamas"), + ENTRY_ISO_3166(Panama, "Panama"), + ENTRY_ISO_3166(Suriname, "Suriname"), + ENTRY_ISO_3166(Haiti, "Haiti"), + ENTRY_ISO_3166(HeardAndMcDonaldIslands, "Heard Island and McDonald Islands"), + ENTRY_ISO_3166(TrinidadAndTobago, "Trinidad and Tobago"), + ENTRY_ISO_3166(PuertoRico, "Puerto Rico"), + ENTRY_ISO_3166(Chile, "Chile"), + ENTRY_ISO_3166(DominicanRepublic, "Dominican Republic"), + ENTRY_ISO_3166(SaintBarthelemy, "Saint Barthélemy"), + ENTRY_ISO_3166(SaintKittsAndNevis, "Saint Kitts and Nevis"), + ENTRY_ISO_3166(SaintLucia, "Saint Lucia"), + ENTRY_ISO_3166(UnitedStatesVirginIslands, "Virgin Islands, U.S."), + ENTRY_ISO_3166(SaintVincentAndTheGrenadines, "Saint Vincent and the Grenadines"), + ENTRY_ISO_3166(Honduras, "Honduras"), + ENTRY_ISO_3166(BritishVirginIslands, "Virgin Islands, British"), + ENTRY_ISO_3166(Antarctica, "Antarctica"), + ENTRY_ISO_3166(Australia, "Australia"), + ENTRY_ISO_3166(SvalbardAndJanMayenIslands, "Svalbard and Jan Mayen"), + ENTRY_ISO_3166(Yemen, "Yemen"), + ENTRY_ISO_3166(Kazakhstan, "Kazakhstan"), + ENTRY_ISO_3166(Jordan, "Jordan"), + ENTRY_ISO_3166(Russia, "Russian Federation"), + ENTRY_ISO_3166(Turkmenistan, "Turkmenistan"), + ENTRY_ISO_3166(Iraq, "Iraq"), + ENTRY_ISO_3166(Bahrain, "Bahrain"), + ENTRY_ISO_3166(Azerbaijan, "Azerbaijan"), + ENTRY_ISO_3166(Thailand, "Thailand"), + ENTRY_ISO_3166(Lebanon, "Lebanon"), + ENTRY_ISO_3166(Kyrgyzstan, "Kyrgyzstan"), + ENTRY_ISO_3166(Brunei, "Brunei Darussalam"), + ENTRY_ISO_3166(Mongolia, "Mongolia"), + ENTRY_ISO_3166(China, "China"), + ENTRY_ISO_3166(SriLanka, "Sri Lanka"), + ENTRY_ISO_3166(Syria, "Syrian Arab Republic"), + ENTRY_ISO_3166(Bangladesh, "Bangladesh"), + ENTRY_ISO_3166(EastTimor, "Timor-Leste"), + ENTRY_ISO_3166(UnitedArabEmirates, "United Arab Emirates"), + ENTRY_ISO_3166(Tajikistan, "Tajikistan"), + ENTRY_ISO_3166(PalestinianTerritories, "Palestine, State of"), + ENTRY_ISO_3166(Vietnam, "Vietnam"), + ENTRY_ISO_3166(HongKong, "Hong Kong"), + ENTRY_ISO_3166(Indonesia, "Indonesia"), + ENTRY_ISO_3166(Israel, "Israel"), + ENTRY_ISO_3166(Afghanistan, "Afghanistan"), + ENTRY_ISO_3166(Pakistan, "Pakistan"), + ENTRY_ISO_3166(Nepal, "Nepal"), + ENTRY_ISO_3166(India, "India"), + ENTRY_ISO_3166(Malaysia, "Malaysia"), + ENTRY_ISO_3166(Kuwait, "Kuwait"), + ENTRY_ISO_3166(Macau, "Macao"), + ENTRY_ISO_3166(Philippines, "Philippines"), + ENTRY_ISO_3166(Oman, "Oman"), + ENTRY_ISO_3166(Cyprus, "Cyprus"), + ENTRY_ISO_3166(Cambodia, "Cambodia"), + ENTRY_ISO_3166(NorthKorea, "Korea, Democratic People's Republic of"), + ENTRY_ISO_3166(Qatar, "Qatar"), + ENTRY_ISO_3166(Myanmar, "Myanmar"), + ENTRY_ISO_3166(SaudiArabia, "Saudi Arabia"), + ENTRY_ISO_3166(Uzbekistan, "Uzbekistan"), + ENTRY_ISO_3166(SouthKorea, "Korea, Republic of"), + ENTRY_ISO_3166(Singapore, "Singapore"), + ENTRY_ISO_3166(Taiwan, "Taiwan"), + ENTRY_ISO_3166(Georgia, "Georgia"), + ENTRY_ISO_3166(Iran, "Iran, Islamic Republic of"), + ENTRY_ISO_3166(Bhutan, "Bhutan"), + ENTRY_ISO_3166(Japan, "Japan"), + ENTRY_ISO_3166(Laos, "Lao People's Democratic Republic"), + ENTRY_ISO_3166(Armenia, "Armenia"), + ENTRY_ISO_3166(Portugal, "Portugal"), + ENTRY_ISO_3166(Bermuda, "Bermuda"), + ENTRY_ISO_3166(CapeVerde, "Cabo Verde"), + ENTRY_ISO_3166(FaroeIslands, "Faroe Islands"), + ENTRY_ISO_3166(Iceland, "Iceland"), + ENTRY_ISO_3166(SouthGeorgiaAndTheSouthSandwichIslands, "South Georgia and the South Sandwich Islands"), + ENTRY_ISO_3166(SaintHelena, "Saint Helena, Ascension and Tristan da Cunha"), + ENTRY_ISO_3166(FalklandIslands, "Falkland Islands (Malvinas)"), + ENTRY_ISO_3166(Netherlands, "Netherlands"), + ENTRY_ISO_3166(Andorra, "Andorra"), + ENTRY_ISO_3166(Greece, "Greece"), + ENTRY_ISO_3166(Serbia, "Serbia"), + ENTRY_ISO_3166(Germany, "Germany"), + ENTRY_ISO_3166(Slovakia, "Slovakia"), + ENTRY_ISO_3166(Belgium, "Belgium"), + ENTRY_ISO_3166(Romania, "Romania"), + ENTRY_ISO_3166(Hungary, "Hungary"), + ENTRY_ISO_3166(Moldova, "Moldova"), + ENTRY_ISO_3166(Denmark, "Denmark"), + ENTRY_ISO_3166(Ireland, "Ireland"), + ENTRY_ISO_3166(Gibraltar, "Gibraltar"), + ENTRY_ISO_3166(Guernsey, "Guernsey"), + ENTRY_ISO_3166(Finland, "Finland"), + ENTRY_ISO_3166(IsleOfMan, "Isle of Man"), + ENTRY_ISO_3166(Turkey, "Turkey"), + ENTRY_ISO_3166(Jersey, "Jersey"), + ENTRY_ISO_3166(Ukraine, "Ukraine"), + ENTRY_ISO_3166(Slovenia, "Slovenia"), + ENTRY_ISO_3166(UnitedKingdom, "United Kingdom"), + ENTRY_ISO_3166(Luxembourg, "Luxembourg"), + ENTRY_ISO_3166(Malta, "Malta"), + ENTRY_ISO_3166(AlandIslands, "Åland Islands"), + ENTRY_ISO_3166(Belarus, "Belarus"), + ENTRY_ISO_3166(Monaco, "Monaco"), + ENTRY_ISO_3166(Norway, "Norway"), + ENTRY_ISO_3166(France, "France"), + ENTRY_ISO_3166(Montenegro, "Montenegro"), + ENTRY_ISO_3166(CzechRepublic, "Czechia"), + ENTRY_ISO_3166(Latvia, "Latvia"), + ENTRY_ISO_3166(Italy, "Italy"), + ENTRY_ISO_3166(SanMarino, "San Marino"), + ENTRY_ISO_3166(BosniaAndHerzegowina, "Bosnia and Herzegovina"), + ENTRY_ISO_3166(Macedonia, "Macedonia, Republic of"), + ENTRY_ISO_3166(Bulgaria, "Bulgaria"), + ENTRY_ISO_3166(Sweden, "Sweden"), + ENTRY_ISO_3166(Estonia, "Estonia"), + ENTRY_ISO_3166(Albania, "Albania"), + ENTRY_ISO_3166(Liechtenstein, "Liechtenstein"), + ENTRY_ISO_3166(VaticanCityState, "Holy See (Vatican City State)"), + ENTRY_ISO_3166(Austria, "Austria"), + ENTRY_ISO_3166(Lithuania, "Lithuania"), + ENTRY_ISO_3166(Poland, "Poland"), + ENTRY_ISO_3166(Croatia, "Croatia"), + ENTRY_ISO_3166(Switzerland, "Switzerland"), + ENTRY_ISO_3166(Madagascar, "Madagascar"), + ENTRY_ISO_3166(BritishIndianOceanTerritory, "British Indian Ocean Territory"), + ENTRY_ISO_3166(ChristmasIsland, "Christmas Island"), + ENTRY_ISO_3166(CocosIslands, "Cocos (Keeling) Islands"), + ENTRY_ISO_3166(Comoros, "Comoros"), + ENTRY_ISO_3166(FrenchSouthernTerritories, "French Southern Territories"), + ENTRY_ISO_3166(Seychelles, "Seychelles"), + ENTRY_ISO_3166(Maldives, "Maldives"), + ENTRY_ISO_3166(Mauritius, "Mauritius"), + ENTRY_ISO_3166(Mayotte, "Mayotte"), + ENTRY_ISO_3166(Reunion, "Réunion"), + ENTRY_ISO_3166(Samoa, "Samoa"), + ENTRY_ISO_3166(NewZealand, "New Zealand"), + ENTRY_ISO_3166(Micronesia, "Micronesia, Federated States of"), + ENTRY_ISO_3166(Vanuatu, "Vanuatu"), + ENTRY_ISO_3166(Kiribati, "Kiribati"), + ENTRY_ISO_3166(TokelauCountry, "Tokelau"), + ENTRY_ISO_3166(Fiji, "Fiji"), + ENTRY_ISO_3166(TuvaluCountry, "Tuvalu"), + ENTRY_ISO_3166(FrenchPolynesia, "French Polynesia"), + ENTRY_ISO_3166(SolomonIslands, "Solomon Islands"), + ENTRY_ISO_3166(Guam, "Guam"), + ENTRY_ISO_3166(UnitedStatesMinorOutlyingIslands, "United States Minor Outlying Islands"), + ENTRY_ISO_3166(MarshallIslands, "Marshall Islands"), + ENTRY_ISO_3166(NauruCountry, "Nauru"), + ENTRY_ISO_3166(Niue, "Niue"), + ENTRY_ISO_3166(NorfolkIsland, "Norfolk Island"), + ENTRY_ISO_3166(NewCaledonia, "New Caledonia"), + ENTRY_ISO_3166(AmericanSamoa, "American Samoa"), + ENTRY_ISO_3166(Palau, "Palau"), + ENTRY_ISO_3166(Pitcairn, "Pitcairn"), + ENTRY_ISO_3166(PapuaNewGuinea, "Papua New Guinea"), + ENTRY_ISO_3166(CookIslands, "Cook Islands"), + ENTRY_ISO_3166(NorthernMarianaIslands, "Northern Mariana Islands"), + ENTRY_ISO_3166(Tonga, "Tonga"), + ENTRY_ISO_3166(WallisAndFutunaIslands, "Wallis and Futuna") + // {QLocale::Default, i18nc("This is a country name associated with a particular time zone in a zone selection dialog", + // "Default")} }, + }); +#undef ENTRY_ISO_3166 + + m_i18nContinents = + QHash({{QStringLiteral("Africa"), i18nc("This is a continent/area associated with a particular timezone", "Africa")}, + {QStringLiteral("America"), i18nc("This is a continent/area associated with a particular timezone", "America")}, + {QStringLiteral("Antarctica"), i18nc("This is a continent/area associated with a particular timezone", "Antarctica")}, + {QStringLiteral("Asia"), i18nc("This is a continent/area associated with a particular timezone", "Asia")}, + {QStringLiteral("Atlantic"), i18nc("This is a continent/area associated with a particular timezone", "Atlantic")}, + {QStringLiteral("Australia"), i18nc("This is a continent/area associated with a particular timezone", "Australia")}, + {QStringLiteral("Europe"), i18nc("This is a continent/area associated with a particular timezone", "Europe")}, + {QStringLiteral("Indian"), i18nc("This is a continent/area associated with a particular timezone", "Indian")}, + {QStringLiteral("Pacific"), i18nc("This is a continent/area associated with a particular timezone", "Pacific")}}); + + m_isInitialized = true; +} diff --git a/kcms/time/timezonesi18n.h b/kcms/time/timezonesi18n.h new file mode 100644 index 00000000..34c922bc --- /dev/null +++ b/kcms/time/timezonesi18n.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Klapetek + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#ifndef TIMEZONESI18N_H +#define TIMEZONESI18N_H + +#include +#include +#include + +class TimezonesI18n : public QObject +{ + Q_OBJECT + +public: + explicit TimezonesI18n(QObject *parent = nullptr); + Q_INVOKABLE QString i18nCity(const QString &city); + Q_INVOKABLE QString i18nContinents(const QString &continent); + Q_INVOKABLE QString i18nCountry(QLocale::Country country); + +private: + void init(); + + QHash m_i18nCities; + QHash m_i18nContinents; + QHash m_i18nCountries; + bool m_isInitialized; +}; + +#endif // TIMEZONESI18N_H diff --git a/kcms/virtualkeyboard/CMakeLists.txt b/kcms/virtualkeyboard/CMakeLists.txt new file mode 100644 index 00000000..afdd4f81 --- /dev/null +++ b/kcms/virtualkeyboard/CMakeLists.txt @@ -0,0 +1,27 @@ +set(MALIIT_KEYBOARD_LANGUAGES_DIR "${CMAKE_INSTALL_FULL_LIBDIR}/maliit/keyboard2/languages" CACHE PATH "Directory containing maliit-keyboard data") + +add_definitions(-DQT_NO_KEYWORDS) + +set(onscreenkeyboard_SRCS + languagemodel.cpp + gsettingsitem.cpp + virtualkeyboard.cpp +) + +add_library(kcm_mobile_onscreenkeyboard MODULE ${onscreenkeyboard_SRCS}) + +target_compile_definitions(kcm_mobile_onscreenkeyboard PRIVATE "MALIIT_KEYBOARD_LANGUAGES_DIR=\"${MALIIT_KEYBOARD_LANGUAGES_DIR}\"") + +target_link_libraries(kcm_mobile_onscreenkeyboard + Qt::Core + KF6::CoreAddons + KF6::I18n + KF6::QuickAddons + KF6::ConfigCore + PkgConfig::GIO + PkgConfig::GOBJECT +) + +install(TARGETS kcm_mobile_onscreenkeyboard DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) + +kpackage_install_package(package kcm_mobile_virtualkeyboard kcms) diff --git a/kcms/virtualkeyboard/Messages.sh b/kcms/virtualkeyboard/Messages.sh new file mode 100644 index 00000000..ac978b2b --- /dev/null +++ b/kcms/virtualkeyboard/Messages.sh @@ -0,0 +1 @@ +$XGETTEXT $(find . -name \*.cpp -o -name \*.h -o -name \*.qml) -o $podir/kcm_mobile_virtualkeyboard.pot diff --git a/kcms/virtualkeyboard/gsettingsitem.cpp b/kcms/virtualkeyboard/gsettingsitem.cpp new file mode 100644 index 00000000..53057bac --- /dev/null +++ b/kcms/virtualkeyboard/gsettingsitem.cpp @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2018 Nicolas Fella + + SPDX-License-Identifier: LGPL-2.1-only + +*/ + +#include +#include + +#include "gsettingsitem.h" + +QVariant GSettingsItem::value(const QString &key) const +{ + if (!m_settings) { + return QVariant(); + } + + GVariant *gvalue = g_settings_get_value(m_settings, key.toLatin1().data()); + + QVariant toReturn; + + switch (g_variant_classify(gvalue)) { + case G_VARIANT_CLASS_BOOLEAN: + toReturn = QVariant(static_cast(g_variant_get_boolean(gvalue))); + break; + case G_VARIANT_CLASS_STRING: + toReturn = QVariant(QString::fromUtf8(g_variant_get_string(gvalue, nullptr))); + break; + case G_VARIANT_CLASS_ARRAY: + if (g_variant_is_of_type(gvalue, G_VARIANT_TYPE_STRING_ARRAY)) { + GVariantIter iter; + QStringList list; + const gchar *str; + g_variant_iter_init(&iter, gvalue); + while (g_variant_iter_next(&iter, "&s", &str)) { + list.append(str); + } + return QVariant(list); + } + break; + default: + qWarning() << "Unhandled variant type in value()"; + } + + g_variant_unref(gvalue); + + return toReturn; +} + +void GSettingsItem::set(const QString &key, const QVariant &val) +{ + if (!m_settings) { + return; + } + + // It might be hard to detect the right GVariant type from + // complex QVariant types such as string lists or more detailed + // types such as integers (GVariant has different sizes), + // therefore we get the current value for the key and convert + // to QVariant using the GVariant type + GVariant *oldValue = g_settings_get_value(m_settings, key.toLatin1().data()); + GVariant *newValue = nullptr; + + switch (g_variant_type_peek_string(g_variant_get_type(oldValue))[0]) { + case G_VARIANT_CLASS_BOOLEAN: + newValue = g_variant_new_boolean(val.toBool()); + break; + case G_VARIANT_CLASS_STRING: + newValue = g_variant_new_string(val.toString().toUtf8().constData()); + break; + case G_VARIANT_CLASS_ARRAY: + if (g_variant_is_of_type(oldValue, G_VARIANT_TYPE_STRING_ARRAY)) { + const QStringList list = val.toStringList(); + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE_STRING_ARRAY); + for (const QString &string : list) { + g_variant_builder_add(&builder, "s", string.toUtf8().constData()); + } + + newValue = g_variant_builder_end(&builder); + break; + } + qWarning() << "Unhandled variant array type in set()"; + break; + default: + qWarning() << "Unhandled variant type in set()"; + } + + if (newValue) { + g_settings_set_value(m_settings, key.toLatin1().data(), newValue); + } + + g_variant_unref(oldValue); +} + +bool GSettingsItem::isValid() const +{ + return m_settings; +} + +GSettingsItem::GSettingsItem(const QString &key, QObject *parent) + : QObject(parent) +{ + const char schemaId[] = "org.maliit.keyboard.maliit"; + + // g_settings_new_with_path asserts if the schema doesn't exist, check this manually to avoid an abort. + auto *defaultSource = g_settings_schema_source_get_default(); + if (!defaultSource) { + qWarning() << "No GSettings schemas are installed on the system"; + return; + } + + auto *schema = g_settings_schema_source_lookup(defaultSource, schemaId, true /*recursive*/); + if (!schema) { + qWarning() << "Settings schema" << schemaId << "is not installed"; + return; + } + + m_settings = g_settings_new_with_path(schemaId, key.toLatin1().data()); + g_settings_schema_unref(schema); + + g_signal_connect(m_settings, "changed", G_CALLBACK(GSettingsItem::settingChanged), this); +} + +GSettingsItem::~GSettingsItem() +{ + g_settings_sync(); + if (m_settings) + g_object_unref(m_settings); +} diff --git a/kcms/virtualkeyboard/gsettingsitem.h b/kcms/virtualkeyboard/gsettingsitem.h new file mode 100644 index 00000000..176ec52b --- /dev/null +++ b/kcms/virtualkeyboard/gsettingsitem.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2018 Nicolas Fella + + SPDX-License-Identifier: LGPL-2.1-only + +*/ + +#ifndef GSETTINGSITEM_H +#define GSETTINGSITEM_H + +#include +#include +#include + +#include + +class GSettingsItem : public QObject +{ + Q_OBJECT + +public: + explicit GSettingsItem(const QString &key, QObject *parent = nullptr); + virtual ~GSettingsItem() override; + + QVariant value(const QString &key) const; + void set(const QString &key, const QVariant &val); + + bool isValid() const; + +Q_SIGNALS: + void subtreeChanged(); + +private: + GSettings *m_settings = nullptr; + + static void settingChanged(GSettings *settings, const gchar *key, gpointer data) + { + Q_UNUSED(settings) + Q_UNUSED(key) + + GSettingsItem *self = static_cast(data); + Q_EMIT self->subtreeChanged(); + } +}; + +#endif // GCONFITEM_H diff --git a/kcms/virtualkeyboard/languagemodel.cpp b/kcms/virtualkeyboard/languagemodel.cpp new file mode 100644 index 00000000..4a5ab5a3 --- /dev/null +++ b/kcms/virtualkeyboard/languagemodel.cpp @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include +#include +#include + +#include "gsettingsitem.h" +#include "languagemodel.h" + +LanguageModel::LanguageModel(QObject *parent, GSettingsItem *settings) + : QAbstractListModel(parent) + , m_gsettings(settings) +{ + beginResetModel(); + loadPlugins(); + endResetModel(); +} + +void LanguageModel::loadPlugins() +{ + const QStringList enabledLangs = m_gsettings->value("enabled-languages").toStringList(); + + QStringList langPaths; + QDirIterator it(QStringLiteral(MALIIT_KEYBOARD_LANGUAGES_DIR), {"*plugin.so"}, QDir::NoFilter, QDirIterator::Subdirectories); + while (it.hasNext()) { + langPaths << it.next(); + } + m_languages.clear(); + for (const auto &langPath : qAsConst(langPaths)) { + QPluginLoader langPlugin(langPath); + const auto &metadata = langPlugin.metaData().value("MetaData").toObject(); + Data lang; + lang.langName = metadata.value("Language").toString(); + lang.langCode = metadata.value("LanguageId").toString(); + lang.enabled = enabledLangs.contains(lang.langCode); + m_languages.append(lang); + } +} + +QVariant LanguageModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + if (index.row() >= m_languages.size()) { + return QVariant(); + } + + const Data data = m_languages.at(index.row()); + switch (role) { + case EnabledRole: + return data.enabled; + case NameRole: + return data.langName; + case LanguageIdRole: + return data.langCode; + } + + return QVariant(); +} + +bool LanguageModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return QAbstractListModel::setData(index, value, role); + } + + if (role == EnabledRole) { + Data &data = m_languages[index.row()]; + if (data.enabled != value.toBool()) { + data.enabled = value.toBool(); + } + Q_EMIT dataChanged(this->index(index.row(), 0), this->index(index.row(), 0)); + } + + QStringList enabledLangs; + for (const auto &data : qAsConst(m_languages)) { + if (data.enabled) { + enabledLangs << data.langCode; + } + } + m_gsettings->set("enabled-languages", enabledLangs); + return QAbstractListModel::setData(index, value, role); +} + +int LanguageModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_languages.size(); +} + +QHash LanguageModel::roleNames() const +{ + return { + {NameRole, "name"}, + {EnabledRole, "enabled"}, + {LanguageIdRole, "langId"}, + }; +} diff --git a/kcms/virtualkeyboard/languagemodel.h b/kcms/virtualkeyboard/languagemodel.h new file mode 100644 index 00000000..87de9c98 --- /dev/null +++ b/kcms/virtualkeyboard/languagemodel.h @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef LANGUAGEMODEL_H +#define LANGUAGEMODEL_H + +#include "gsettingsitem.h" +#include + +struct Data { + QString langCode; + QString langName; + bool enabled; +}; + +class LanguageModel : public QAbstractListModel +{ + enum ModelRoles { + NameRole = Qt::DisplayRole, + EnabledRole = Qt::UserRole + 1, + LanguageIdRole, + }; + + Q_OBJECT +public: + LanguageModel(QObject *parent, GSettingsItem *gsettingsItem); + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + QHash roleNames() const override; + +private: + QVector m_languages; + void loadPlugins(); + GSettingsItem *m_gsettings; +}; + +#endif diff --git a/kcms/virtualkeyboard/metadata.json b/kcms/virtualkeyboard/metadata.json new file mode 100644 index 00000000..ed49cb2d --- /dev/null +++ b/kcms/virtualkeyboard/metadata.json @@ -0,0 +1,153 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Bhushan Shah", + "Name[az]": "Bhushan Shah", + "Name[ca@valencia]": "Bhushan Shah", + "Name[ca]": "Bhushan Shah", + "Name[cs]": "Bhushan Shah", + "Name[de]": "Bhushan Shah", + "Name[en_GB]": "Bhushan Shah", + "Name[es]": "Bhushan Shah", + "Name[eu]": "Bhushan Shah", + "Name[fi]": "Bhushan Shah", + "Name[fr]": "Bhushan Shah", + "Name[hu]": "Bhushan Shah", + "Name[ia]": "Bhushan Shah", + "Name[id]": "Bhushan Shah", + "Name[is]": "Bhushan Shah", + "Name[it]": "Bhushan Shah", + "Name[ka]": "Bhushan Shah", + "Name[ko]": "Bhushan Shah", + "Name[lt]": "Bhushan Shah", + "Name[nl]": "Bhushan Shah", + "Name[nn]": "Bhushan Shah", + "Name[pa]": "ਭੂਸ਼ਨ ਸ਼ਾਹ", + "Name[pl]": "Bhushan Shah", + "Name[pt]": "Bhushan Shah", + "Name[pt_BR]": "Bhushan Shah", + "Name[ro]": "Bhushan Shah", + "Name[ru]": "Bhushan Shah", + "Name[sl]": "Bhushan Shah", + "Name[sv]": "Bhushan Shah", + "Name[tr]": "Buşan Şah", + "Name[uk]": "Bhushan Shah", + "Name[vi]": "Bhushan Shah", + "Name[x-test]": "xxBhushan Shahxx", + "Name[zh_CN]": "Bhushan Shah", + "Name[zh_TW]": "Bhushan Shah" + } + ], + "Description": "On-Screen Keyboard configuration", + "Description[az]": "Ekran klaviaturası tənzimləməsi", + "Description[ca@valencia]": "Configureu el teclat en pantalla", + "Description[ca]": "Configuració del teclat en pantalla", + "Description[cs]": "Nastavení klávesnice na obrazovce", + "Description[de]": "Einrichtung der Bildschirmtastatur", + "Description[en_GB]": "On-Screen Keyboard configuration", + "Description[es]": "Configuración del teclado en pantalla", + "Description[eu]": "Pantailako teklatua konfiguratzea", + "Description[fi]": "Näyttönäppäimistön asetukset", + "Description[fr]": "Configuration du clavier virtuel", + "Description[hu]": "Képernyő-billentyűzet beállítása", + "Description[ia]": "Configuration de Claviero sur le schermo", + "Description[id]": "Konfigurasi On-Screen Keyboard", + "Description[is]": "Grunnstillingar skjályklaborðs", + "Description[it]": "Configurazione della tastiera su schermo", + "Description[ka]": "კლავიატურის კრანზე მორგება", + "Description[ko]": "가상 키보드 설정", + "Description[lt]": "Ekraninės klaviatūros konfigūracija", + "Description[nl]": "Configuratie van toetsenbord op het scherm", + "Description[nn]": "Oppsett av skjermtastatur", + "Description[pa]": "ਆਨ-ਸਕਰੀਨ ਕੀਬੋਰਡ ਦੀ ਸੰਰਚਨਾ", + "Description[pl]": "Ustawienia klawiatury ekranowej", + "Description[pt]": "Configuração do Teclado Virtual", + "Description[pt_BR]": "Configuração do teclado virtual", + "Description[ro]": "Configurarea tastaturii pe ecran", + "Description[ru]": "Настройка экранной клавиатуры", + "Description[sl]": "Nastavitev tipkovnice na zaslonu", + "Description[sv]": "Inställning av skärmtangentbord", + "Description[tr]": "Ekran Klavyesi yapılandırması", + "Description[uk]": "Налаштування екранної клавіатури", + "Description[vi]": "Cấu hình bàn phím trên màn hình", + "Description[x-test]": "xxOn-Screen Keyboard configurationxx", + "Description[zh_CN]": "屏幕键盘配置", + "Description[zh_TW]": "螢幕上的虛擬鍵盤設定", + "FormFactors": [ + "handset", + "tablet" + ], + "Icon": "input-keyboard", + "License": "GPL", + "Name": "On-Screen Keyboard", + "Name[az]": "Ekran klaviaturası", + "Name[ca@valencia]": "Teclat en pantalla", + "Name[ca]": "Teclat en pantalla", + "Name[cs]": "Klávesnice na obrazovce", + "Name[de]": "Bildschirmtastatur", + "Name[en_GB]": "On-Screen Keyboard", + "Name[es]": "Teclado en pantalla", + "Name[eu]": "Pantailako teklatua", + "Name[fi]": "Näyttönäppäimistö", + "Name[fr]": "Clavier virtuel", + "Name[hu]": "Képernyő-billentyűzet", + "Name[ia]": "Claviero sur schermo", + "Name[id]": "On-Screen Keyboard", + "Name[is]": "Skjályklaborð", + "Name[it]": "Tastiera su schermo", + "Name[ka]": "ეკრანის კლავიატურა", + "Name[ko]": "가상 키보드", + "Name[lt]": "Ekraninė klaviatūra", + "Name[nl]": "Toetsenbord op scherm", + "Name[nn]": "Skjermtastatur", + "Name[pa]": "ਆਨ-ਸਕਰੀਨ ਕੀਬੋਰਡ", + "Name[pl]": "Klawiatura ekranowa", + "Name[pt]": "Teclado no Ecrã", + "Name[pt_BR]": "Teclado virtual", + "Name[ro]": "Tastatură pe ecran", + "Name[ru]": "Экранная клавиатура", + "Name[sl]": "Tipkovnica na zaslonu", + "Name[sv]": "Tangentbord på skärmen", + "Name[tr]": "Ekran Klavyesi", + "Name[uk]": "Екранна клавіатура", + "Name[vi]": "Bàn phím trên màn hình", + "Name[x-test]": "xxOn-Screen Keyboardxx", + "Name[zh_CN]": "屏幕键盘", + "Name[zh_TW]": "螢幕上的虛擬鍵盤", + "Version": "1.0", + "Website": "https://plasma-mobile.org" + }, + "X-KDE-Keywords": "onscreen,osk,virtualkeyboard,keyboard", + "X-KDE-Keywords[az]": "virtualkeyboard,keyboard,virtual klaviatura", + "X-KDE-Keywords[ca@valencia]": "teclat virtual,teclat", + "X-KDE-Keywords[ca]": "teclat virtual,teclat", + "X-KDE-Keywords[cs]": "virtuální klávesnice, klávesnice", + "X-KDE-Keywords[de]": "Virtuelle Tastatur,Tastatur", + "X-KDE-Keywords[en_GB]": "virtualkeyboard,keyboard", + "X-KDE-Keywords[es]": "tecladovirtual,teclado", + "X-KDE-Keywords[eu]": "alegiazko teklatua,teklatua", + "X-KDE-Keywords[fi]": "näyttönäppäimistö,virtuaalinäppäimistö,näppäimistö", + "X-KDE-Keywords[fr]": "clavier virtuel, clavier", + "X-KDE-Keywords[hu]": "virtuális billentyűzet,billentyűzet", + "X-KDE-Keywords[ia]": "virtualkeyboard,keyboard", + "X-KDE-Keywords[it]": "tastieravirtuale,tastiera", + "X-KDE-Keywords[ko]": "virtualkeyboard,keyboard,키보드,가상 키보드", + "X-KDE-Keywords[lt]": "virtualiklaviatūra,virtualiklaviatura,virtuali klaviatūra,virtuali klaviatura,klaviatūra,klaviatura", + "X-KDE-Keywords[nl]": "virtueeltoetsenbord,toetsenbord", + "X-KDE-Keywords[nn]": "virtuelt tastatur,tastatur", + "X-KDE-Keywords[pa]": "ਫ਼ਰਜ਼ੀ ਕੀਬੋਰਡ,ਵਰਚੁਅਲ ਕੀਬੋਰਡ,ਖਾਕਾ,ਕੀਬੋਰਡ", + "X-KDE-Keywords[pl]": "klawiatura ekranowa,klawiatura", + "X-KDE-Keywords[pt]": "teclado virtual,teclado", + "X-KDE-Keywords[pt_BR]": "teclado virtual,teclado", + "X-KDE-Keywords[ro]": "tastatură virtuală,tastatură", + "X-KDE-Keywords[ru]": "virtualkeyboard,keyboard,клавиатура,виртуальная клавиатура", + "X-KDE-Keywords[sk]": "virtuálna klávesnica, klávesnica", + "X-KDE-Keywords[sl]": "navidezna tipkovnica,tipkovnica", + "X-KDE-Keywords[sv]": "virtuellt tangentbord,tangentbord", + "X-KDE-Keywords[ta]": "virtualkeyboard, keyboard, மெய்நிகர் விசைப்பலகை, விசைப்பலகை, திரையில் விசைப்பலகை, திரை விசைப்பலகை, திரைவிசைப்பலகை", + "X-KDE-Keywords[uk]": "virtualkeyboard,keyboard,екранна,віртуальна,клавіатура,клавіші", + "X-KDE-Keywords[vi]": "virtualkeyboard,keyboard,bàn phím ảo,bàn phím", + "X-KDE-Keywords[x-test]": "xxvirtualkeyboardxx,xxkeyboardxx", + "X-KDE-Keywords[zh_CN]": "virtualkeyboard,keyboard,虚拟键盘,键盘" +} diff --git a/kcms/virtualkeyboard/package/contents/ui/languages.qml b/kcms/virtualkeyboard/package/contents/ui/languages.qml new file mode 100644 index 00000000..f8a67642 --- /dev/null +++ b/kcms/virtualkeyboard/package/contents/ui/languages.qml @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.11 as QQC2 + +import org.kde.kirigami 2.10 as Kirigami +import org.kde.kcm 1.3 as KCM +import org.kde.kitemmodels 1.0 as KItemModel + +import org.kde.kcm.virtualkeyboard 1.0 + +KCM.ScrollViewKCM { + id: root + + title: i18n("Languages") + + view: ListView { + id: languageList + + clip: true + + model: KItemModel.KSortFilterProxyModel { + sourceModel: kcm.languageModel + sortRole: "name" + sortOrder: Qt.Ascending + } + + delegate: Kirigami.AbstractListItem { + QQC2.CheckBox { + text: model.name + checked: model.enabled + onCheckedChanged: { + model.enabled = checked + } + } + } + } + + footer: RowLayout { + QQC2.Button { + text: i18n("Apply") + icon.name: "dialog-ok" + onClicked: kcm.pop() + Layout.alignment: Qt.AlignRight + } + } +} diff --git a/kcms/virtualkeyboard/package/contents/ui/main.qml b/kcms/virtualkeyboard/package/contents/ui/main.qml new file mode 100644 index 00000000..e2c71b5b --- /dev/null +++ b/kcms/virtualkeyboard/package/contents/ui/main.qml @@ -0,0 +1,150 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + SPDX-FileCopyrightText: 2021 Devin Lin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick 2.7 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.11 as QQC2 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kcm 1.3 as KCM +import org.kde.kitemmodels 1.0 as KItemModel +import org.kde.kirigamiaddons.labs.mobileform 0.1 as MobileForm + +KCM.SimpleKCM { + id: root + + title: i18n("On-Screen Keyboard") + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.gridUnit + bottomPadding: Kirigami.Units.gridUnit + + ColumnLayout { + spacing: 0 + width: parent.width + + MobileForm.FormCard { + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.AbstractFormDelegate { + Layout.fillWidth: true + background: Item {} + contentItem: RowLayout { + QQC2.TextField { + Layout.fillWidth: true + placeholderText: i18n("Type anything here…") + } + } + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: "Feedback" + } + + MobileForm.FormSwitchDelegate { + id: firstFeedbackCheckBox + text: i18n("Sound") + description: i18n("Whether to emit a sound on keypress.") + checked: kcm.soundFeedback + onCheckedChanged: kcm.soundFeedback = checked; + } + + MobileForm.FormDelegateSeparator { above: firstFeedbackCheckBox; below: secondFeedbackCheckBox } + + MobileForm.FormSwitchDelegate { + id: secondFeedbackCheckBox + text: i18n("Vibration") + description: i18n("Whether to vibrate on keypress.") + checked: kcm.vibrateFeedback + onCheckedChanged: kcm.vibrateFeedback = checked; + } + } + } + + MobileForm.FormCard { + Layout.topMargin: Kirigami.Units.largeSpacing + Layout.fillWidth: true + + contentItem: ColumnLayout { + spacing: 0 + + MobileForm.FormCardHeader { + title: "Text Correction" + } + + MobileForm.FormCheckDelegate { + id: firstTextCorrectionCheckBox + text: i18n("Check spelling of entered text") + checked: kcm.spellCheck + onCheckedChanged: kcm.spellCheck = checked; + } + + MobileForm.FormDelegateSeparator { above: firstTextCorrectionCheckBox; below: capitalizeCheck } + + MobileForm.FormCheckDelegate { + id: capitalizeCheck + text: i18n("Capitalize the first letter of each sentence") + checked: kcm.autoCapitalize + onCheckedChanged: kcm.autoCapitalize = checked; + } + + MobileForm.FormDelegateSeparator { above: capitalizeCheck; below: wordCompletionCheck } + + MobileForm.FormCheckDelegate { + id: wordCompletionCheck + text: i18n("Complete current word with first suggestion when hitting space") + checked: kcm.autoCompleteOnSpace + onCheckedChanged: kcm.autoCompleteOnSpace = checked; + } + + MobileForm.FormDelegateSeparator { above: wordCompletionCheck; below: wordSuggestionCheck } + + MobileForm.FormCheckDelegate { + id: wordSuggestionCheck + text: i18n("Suggest potential words in word ribbon") + checked: kcm.showSuggestions + onCheckedChanged: { + kcm.showSuggestions = checked; + } + } + + MobileForm.FormDelegateSeparator { above: wordSuggestionCheck; below: fullStopCheck } + + MobileForm.FormCheckDelegate { + id: fullStopCheck + text: i18n("Insert a full-stop when space is pressed twice") + checked: kcm.fullStopOnDoubleSpace + onCheckedChanged: { + kcm.fullStopOnDoubleSpace = checked; + } + } + + MobileForm.FormDelegateSeparator { above: fullStopCheck; below: languageButton } + + MobileForm.FormButtonDelegate { + id: languageButton + text: i18n("Configure Languages") + icon.name: "set-language" + onClicked: kcm.push("languages.qml") + } + } + } + } +} diff --git a/kcms/virtualkeyboard/virtualkeyboard.cpp b/kcms/virtualkeyboard/virtualkeyboard.cpp new file mode 100644 index 00000000..36d44697 --- /dev/null +++ b/kcms/virtualkeyboard/virtualkeyboard.cpp @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "virtualkeyboard.h" +#include +#include + +#include + +#include "languagemodel.h" + +// clang-format off + +#define SETTER(setter, member, gsetting, signal) \ + void VirtualKeyboard::setter(bool enabled) \ + { \ + if (member != enabled) {\ + member = enabled; \ + m_gsettings->set(gsetting, enabled); \ + Q_EMIT signal();\ + }\ + } + +// clang-format on + +K_PLUGIN_CLASS_WITH_JSON(VirtualKeyboard, "metadata.json") + +VirtualKeyboard::VirtualKeyboard(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args) + : KQuickAddons::ConfigModule(parent, metaData, args) + , m_gsettings(new GSettingsItem("/org/maliit/keyboard/maliit/", parent)) + , m_langModel(new LanguageModel(this, m_gsettings)) +{ + qmlRegisterAnonymousType("org.kde.kcm.virtualkeyboard", 1); + + m_autoCapitalize = m_gsettings->value("auto-capitalization").toBool(); + m_autoCompleteOnSpace = m_gsettings->value("auto-completion").toBool(); + m_showSuggestions = m_gsettings->value("predictive-text").toBool(); + m_fullStopOnDoubleSpace = m_gsettings->value("double-space-full-stop").toBool(); + m_spellCheck = m_gsettings->value("spell-checking").toBool(); + + m_soundFeedback = m_gsettings->value("key-press-feedback").toBool(); + m_vibrateFeedback = m_gsettings->value("key-press-haptic-feedback").toBool(); +} + +SETTER(setAutoCapitalize, m_autoCapitalize, "auto-capitalization", autoCapitalizeChanged) +SETTER(setAutoCompleteOnSpace, m_autoCompleteOnSpace, "auto-completion", autoCompleteOnSpaceChanged); +SETTER(setShowSuggestions, m_showSuggestions, "predictive-text", showSuggestionsChanged) +SETTER(setFullStopOnDoubleSpace, m_fullStopOnDoubleSpace, "double-space-full-stop", fullStopOnDoubleSpaceChanged) +SETTER(setSpellCheck, m_spellCheck, "spell-checking", spellCheckChanged) + +SETTER(setSoundFeedback, m_soundFeedback, "key-press-feedback", soundFeedbackChanged) +SETTER(setVibrateFeedback, m_vibrateFeedback, "key-press-haptic-feedback", vibrateFeedbackChanged) + +#include "virtualkeyboard.moc" diff --git a/kcms/virtualkeyboard/virtualkeyboard.h b/kcms/virtualkeyboard/virtualkeyboard.h new file mode 100644 index 00000000..091a5736 --- /dev/null +++ b/kcms/virtualkeyboard/virtualkeyboard.h @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "languagemodel.h" +#include + +#include +#include + +#ifndef VIRTUALKEYBOARD_H +#define VIRTUALKEYBOARD_H + +class VirtualKeyboard : public KQuickAddons::ConfigModule +{ + Q_OBJECT + Q_PROPERTY(LanguageModel *languageModel MEMBER m_langModel CONSTANT) + + Q_PROPERTY(bool spellCheck MEMBER m_spellCheck WRITE setSpellCheck NOTIFY spellCheckChanged) + Q_PROPERTY(bool autoCapitalize MEMBER m_autoCapitalize WRITE setAutoCapitalize NOTIFY autoCapitalizeChanged) + Q_PROPERTY(bool autoCompleteOnSpace MEMBER m_autoCompleteOnSpace WRITE setAutoCompleteOnSpace NOTIFY autoCompleteOnSpaceChanged) + Q_PROPERTY(bool showSuggestions MEMBER m_showSuggestions WRITE setShowSuggestions NOTIFY showSuggestionsChanged) + Q_PROPERTY(bool fullStopOnDoubleSpace MEMBER m_fullStopOnDoubleSpace WRITE setFullStopOnDoubleSpace NOTIFY fullStopOnDoubleSpaceChanged) + + Q_PROPERTY(bool soundFeedback MEMBER m_soundFeedback WRITE setSoundFeedback NOTIFY soundFeedbackChanged) + Q_PROPERTY(bool vibrateFeedback MEMBER m_vibrateFeedback WRITE setVibrateFeedback NOTIFY vibrateFeedbackChanged) + +public: + VirtualKeyboard(QObject *parent, const KPluginMetaData &metaData, const QVariantList &args); + + void setSpellCheck(bool enabled); + void setAutoCapitalize(bool enabled); + void setAutoCompleteOnSpace(bool enabled); + void setShowSuggestions(bool enabled); + void setFullStopOnDoubleSpace(bool enabled); + + void setSoundFeedback(bool enabled); + void setVibrateFeedback(bool enabled); + +Q_SIGNALS: + void spellCheckChanged(); + void autoCapitalizeChanged(); + void autoCompleteOnSpaceChanged(); + void showSuggestionsChanged(); + void fullStopOnDoubleSpaceChanged(); + void soundFeedbackChanged(); + void vibrateFeedbackChanged(); + +private: + GSettingsItem *m_gsettings; + LanguageModel *m_langModel; + + // spell check + bool m_spellCheck; + bool m_autoCapitalize; + bool m_autoCompleteOnSpace; + bool m_showSuggestions; + bool m_fullStopOnDoubleSpace; + + // feedback + bool m_soundFeedback; + bool m_vibrateFeedback; +}; + +#endif