diff --git a/.gitignore b/.gitignore index ae97f4c..4eb9fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ gschemas.compiled out/ *.po~ kwin/src/xrdriveripc/xrdriveripc.py +kwin/VERSION +kwin/build-test/ \ No newline at end of file diff --git a/bin/package_kwin b/bin/package_kwin index b30f076..9d71536 100755 --- a/bin/package_kwin +++ b/bin/package_kwin @@ -62,6 +62,7 @@ cp $XR_DRIVER_DIR/bin/xr_driver_setup $PACKAGE_DIR/bin # alternative to symlinking, since the Docker build can't resolve to the parent directory # this file is in .gitignore so it doesn't get duplicated cp ui/modules/PyXRLinuxDriverIPC/xrdriveripc.py $KWIN_DIR/src/xrdriveripc/xrdriveripc.py +cp VERSION $KWIN_DIR pushd $KWIN_DIR docker-build/init.sh diff --git a/kwin/src/CMakeLists.txt b/kwin/src/CMakeLists.txt index 619ee0b..399840d 100644 --- a/kwin/src/CMakeLists.txt +++ b/kwin/src/CMakeLists.txt @@ -1,4 +1,11 @@ add_subdirectory(xrdriveripc) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/../VERSION" BREEZY_DESKTOP_VERSION_RAW) +if(NOT BREEZY_DESKTOP_VERSION_RAW) + set(BREEZY_DESKTOP_VERSION_RAW "dev") +endif() +string(STRIP "${BREEZY_DESKTOP_VERSION_RAW}" BREEZY_DESKTOP_VERSION) + add_subdirectory(kcm) kcoreaddons_add_plugin(breezy_desktop INSTALL_NAMESPACE "kwin/effects/plugins/") @@ -32,11 +39,12 @@ math(EXPR KWIN_VERSION_ENCODED "${KWIN_VERSION_MAJOR} * 10000 + ${KWIN_VERSION_M # Export as compile definitions. Keep the original string macro as well. target_compile_definitions(breezy_desktop PRIVATE - KWIN_VERSION_STR=\"${KWin_VERSION}\" - KWIN_VERSION_MAJOR=${KWIN_VERSION_MAJOR} - KWIN_VERSION_MINOR=${KWIN_VERSION_MINOR} - KWIN_VERSION_PATCH=${KWIN_VERSION_PATCH} - KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED} + KWIN_VERSION_STR=\"${KWin_VERSION}\" + KWIN_VERSION_MAJOR=${KWIN_VERSION_MAJOR} + KWIN_VERSION_MINOR=${KWIN_VERSION_MINOR} + KWIN_VERSION_PATCH=${KWIN_VERSION_PATCH} + KWIN_VERSION_ENCODED=${KWIN_VERSION_ENCODED} + BREEZY_DESKTOP_VERSION_STR=\"${BREEZY_DESKTOP_VERSION}\" ) target_include_directories(breezy_desktop PRIVATE /usr/include/kwin) target_include_directories(breezy_desktop PRIVATE xrdriveripc) diff --git a/kwin/src/breezydesktopeffect.cpp b/kwin/src/breezydesktopeffect.cpp index 6288baa..aa1c7a5 100644 --- a/kwin/src/breezydesktopeffect.cpp +++ b/kwin/src/breezydesktopeffect.cpp @@ -221,11 +221,11 @@ void BreezyDesktopEffect::deactivate() void BreezyDesktopEffect::enableDriver() { qCCritical(KWIN_XR) << "\t\t\tBreezy - enableDriver"; - XRDriverIPC::instance().writeConfig({ - {"disabled", false}, - {"output_mode", "external_only"}, - {"external_mode", "breezy_desktop"} - }); + QJsonObject obj; + obj.insert(QStringLiteral("disabled"), false); + obj.insert(QStringLiteral("output_mode"), QStringLiteral("external_only")); + obj.insert(QStringLiteral("external_mode"), QStringLiteral("breezy_desktop")); + XRDriverIPC::instance().writeConfig(obj); } void BreezyDesktopEffect::realDeactivate() diff --git a/kwin/src/kcm/CMakeLists.txt b/kwin/src/kcm/CMakeLists.txt index 34e048e..87ceacc 100644 --- a/kwin/src/kcm/CMakeLists.txt +++ b/kwin/src/kcm/CMakeLists.txt @@ -16,3 +16,8 @@ target_link_libraries(breezy_desktop_config xr_driver_ipc ) + +# Ensure the version macro is available to the KCM as well (defined in parent CMakeLists) +if(BREEZY_DESKTOP_VERSION) + target_compile_definitions(breezy_desktop_config PRIVATE BREEZY_DESKTOP_VERSION_STR=\"${BREEZY_DESKTOP_VERSION}\") +endif() diff --git a/kwin/src/kcm/breezydesktopeffectkcm.cpp b/kwin/src/kcm/breezydesktopeffectkcm.cpp index fc0c10d..105163f 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.cpp +++ b/kwin/src/kcm/breezydesktopeffectkcm.cpp @@ -14,8 +14,17 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include Q_LOGGING_CATEGORY(KWIN_XR, "kwin.xr") @@ -72,6 +81,44 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); + + if (auto label = widget()->findChild("labelAppNameVersion")) { + label->setText(QStringLiteral("Breezy Desktop - v%1").arg(QLatin1String(BREEZY_DESKTOP_VERSION_STR))); + } + + if (auto btnEmail = widget()->findChild("buttonSubmitEmail")) { + connect(btnEmail, &QPushButton::clicked, this, [this]() { + auto edit = widget()->findChild("lineEditLicenseEmail"); + auto labelStatus = widget()->findChild("labelEmailStatus"); + if (!edit || edit->text().trimmed().isEmpty() || !labelStatus) return; + setRequestInProgress({edit, sender()}, true); + labelStatus->setVisible(false); + bool success = XRDriverIPC::instance().requestToken(edit->text().trimmed().toStdString()); + showStatus(labelStatus, success, success ? tr("Request sent. Check your email for instructions.") : tr("Failed to send request.")); + setRequestInProgress({edit, sender()}, false); + }); + if (auto emailEdit = widget()->findChild("lineEditLicenseEmail")) { + emailEdit->installEventFilter(this); + } + } + if (auto btnToken = widget()->findChild("buttonSubmitToken")) { + connect(btnToken, &QPushButton::clicked, this, [this]() { + auto edit = widget()->findChild("lineEditLicenseToken"); + auto labelStatus = widget()->findChild("labelTokenStatus"); + if (!edit || edit->text().trimmed().isEmpty() || !labelStatus) return; + setRequestInProgress({edit, sender()}, true); + labelStatus->setVisible(false); + bool success = XRDriverIPC::instance().verifyToken(edit->text().trimmed().toStdString()); + if (success) { + XRDriverIPC::instance().writeControlFlags({{"refresh_device_license", true}}); + } + showStatus(labelStatus, success, success ? tr("Your license has been refreshed.") : tr("Invalid or expired token.")); + setRequestInProgress({edit, sender()}, false); + }); + if (auto tokenEdit = widget()->findChild("lineEditLicenseToken")) { + tokenEdit->installEventFilter(this); + } + } } BreezyDesktopEffectConfig::~BreezyDesktopEffectConfig() @@ -133,11 +180,12 @@ void BreezyDesktopEffectConfig::updateUnmanagedState() void BreezyDesktopEffectConfig::pollDriverState() { auto &bridge = XRDriverIPC::instance(); - auto stateOpt = bridge.retrieveDriverState(); - const XRDict &state = stateOpt.value(); + auto stateJsonOpt = bridge.retrieveDriverState(); + if (!stateJsonOpt) return; + auto stateJson = stateJsonOpt.value(); - m_connectedDeviceBrand = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceBrand)); - m_connectedDeviceModel = QString::fromStdString(XRDriverIPC::string(state, XRStateEntry::ConnectedDeviceModel)); + m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); + m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); const bool wasDeviceConnected = m_deviceConnected; m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty(); @@ -146,6 +194,137 @@ void BreezyDesktopEffectConfig::pollDriverState() QStringLiteral("%1 %2 connected").arg(m_connectedDeviceBrand, m_connectedDeviceModel) : QStringLiteral("No device connected")); } + + refreshLicenseUi(stateJson); +} + +void BreezyDesktopEffectConfig::showStatus(QLabel *label, bool success, const QString &message) { + if (!label) return; + QPalette pal = label->palette(); + pal.setColor(QPalette::WindowText, success ? QColor(Qt::darkGreen) : QColor(Qt::red)); + label->setPalette(pal); + label->setText(message); + label->setVisible(true); +} + +void BreezyDesktopEffectConfig::setRequestInProgress(std::initializer_list widgets, bool inProgress) { + for (auto *obj : widgets) { + if (auto *w = qobject_cast(obj)) { + w->setEnabled(!inProgress); + } + } +} + +bool BreezyDesktopEffectConfig::eventFilter(QObject *watched, QEvent *event) { + if (event->type() == QEvent::KeyPress) { + auto *ke = static_cast(event); + if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) { + if (auto *edit = qobject_cast(watched)) { + // Determine which button to invoke + QString objName = edit->objectName(); + QString buttonName; + if (objName == QLatin1String("lineEditLicenseEmail")) buttonName = QStringLiteral("buttonSubmitEmail"); + else if (objName == QLatin1String("lineEditLicenseToken")) buttonName = QStringLiteral("buttonSubmitToken"); + if (!buttonName.isEmpty()) { + if (auto btn = widget()->findChild(buttonName)) { + // Trigger click but stop further propagation so dialog doesn't accept/close + QMetaObject::invokeMethod(btn, "click", Qt::QueuedConnection); + event->accept(); + return true; // eat event + } + } + } + } + } + return KCModule::eventFilter(watched, event); +} + +static QString secondsToRemainingString(qint64 secs) { + if (secs <= 0) return {}; + + if (secs / 60 < 60) { + return QObject::tr("less than an hour"); + } + if (secs / 3600 < 24) { + qint64 hours = secs / 3600; + if (hours == 1) return QObject::tr("1 hour"); + return QObject::tr("%1 hours").arg(hours); + } + if ((secs / 86400) < 30 ) { + qint64 days = secs / 86400; + if (days == 1) return QObject::tr("1 day"); + return QObject::tr("%1 days").arg(days); + } + return {}; +} + +void BreezyDesktopEffectConfig::refreshLicenseUi(const QJsonObject &rootObj) { + auto tab = widget()->findChild("tabLicenseDetails"); + if (!tab) return; + auto labelSummary = tab->findChild("labelLicenseSummary"); + if (!labelSummary) return; + + QString status = tr("disabled"); + QString renewalDescriptor = QStringLiteral(""); + auto uiView = rootObj.value(QStringLiteral("ui_view")).toObject(); + auto license = uiView.value(QStringLiteral("license")).toObject(); + bool warningState = true; + if (!license.isEmpty()) { + auto tiers = license.value(QStringLiteral("tiers")).toObject(); + QJsonValue prodTier = tiers.value(QStringLiteral("subscriber")); + QJsonObject prodTierObj = prodTier.isUndefined() ? QJsonObject() : prodTier.toObject(); + + auto features = license.value(QStringLiteral("features")).toObject(); + QJsonValue prodFeature = features.value(QStringLiteral("productivity_basic")); + QJsonObject prodFeatureObj = prodFeature.isUndefined() ? QJsonObject() : prodFeature.toObject(); + if (!prodTierObj.isEmpty() && !prodFeatureObj.isEmpty()) { + const QString activePeriod = prodTierObj.value(QStringLiteral("active_period")).toString(); + const bool isActive = !activePeriod.isEmpty(); + if (isActive) { + status = tr("active"); + + QString periodDescriptor = activePeriod.contains(QStringLiteral("lifetime"), Qt::CaseInsensitive) ? + tr("lifetime") : + tr("%1 license").arg(activePeriod); + + QString timeDescriptor; + auto secsVal = prodTierObj.value(QStringLiteral("funds_needed_in_seconds")); + if (secsVal.isDouble()) { + qint64 secs = static_cast(secsVal.toDouble()); + QString remaining = secondsToRemainingString(secs); + if (!remaining.isEmpty()) { + timeDescriptor = tr("%1 remaining").arg(remaining); + } + } + renewalDescriptor = tr(" (%1)").arg(periodDescriptor); + warningState = !timeDescriptor.isEmpty(); + if (warningState) { + auto fundsNeeded = prodTierObj.value(QStringLiteral("funds_needed_by_period")).toObject().value(activePeriod).toDouble(); + if (fundsNeeded > 0.0) { + QString fundsNeededDescriptor = tr("$%1 USD to renew").arg(fundsNeeded); + renewalDescriptor = tr(" (%1, %2, %3)").arg(periodDescriptor, fundsNeededDescriptor, timeDescriptor); + } + } + } else { + QJsonValue isEnabled = prodFeatureObj.value(QStringLiteral("is_enabled")); + QJsonValue isTrial = prodFeatureObj.value(QStringLiteral("is_trial")); + if (isEnabled.toBool() && isTrial.toBool()) { + status = tr("in trial"); + auto secsVal = prodFeatureObj.value(QStringLiteral("funds_needed_in_seconds")); + if (secsVal.isDouble()) { + qint64 secs = static_cast(secsVal.toDouble()); + QString remaining = secondsToRemainingString(secs); + warningState = !remaining.isEmpty(); + if (warningState) { + QString timeDescriptor = tr("%1 remaining").arg(remaining); + renewalDescriptor = tr(" (%1)").arg(timeDescriptor); + } + } + } + } + } + } + labelSummary->setText(tr("Productivity Tier features are %1%2").arg(status, renewalDescriptor)); } #include "breezydesktopeffectkcm.moc" \ No newline at end of file diff --git a/kwin/src/kcm/breezydesktopeffectkcm.h b/kwin/src/kcm/breezydesktopeffectkcm.h index 13d354c..d717916 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.h +++ b/kwin/src/kcm/breezydesktopeffectkcm.h @@ -30,6 +30,10 @@ private: void updateConfigFromUi(); void updateUnmanagedState(); void pollDriverState(); + void refreshLicenseUi(const QJsonObject &rootObj); + void showStatus(QLabel *label, bool success, const QString &message); + void setRequestInProgress(std::initializer_list widgets, bool inProgress); + bool eventFilter(QObject *watched, QEvent *event) override; ::Ui::BreezyDesktopEffectConfig ui; @@ -39,4 +43,5 @@ private: QString m_connectedDeviceBrand; QString m_connectedDeviceModel; QTimer m_statePollTimer; // periodic driver state polling + bool m_licenseLoading = false; }; diff --git a/kwin/src/kcm/breezydesktopeffectkcm.ui b/kwin/src/kcm/breezydesktopeffectkcm.ui index c431a53..0930c71 100644 --- a/kwin/src/kcm/breezydesktopeffectkcm.ui +++ b/kwin/src/kcm/breezydesktopeffectkcm.ui @@ -141,6 +141,167 @@ + + + &License Details + + + + + + + + + true + + + true + + + + + + + Request a token + + + + + + you@example.com + + + + + + + Submit + + + + + + + + + + false + + + true + + + + + + + + + + Verify token + + + + + + + + + Verify + + + + + + + + + + false + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + &About + + + + + + Breezy Desktop Effect - v0.0.0 + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + 14 + 75 + true + + + + + + + + Author: Wayne Heaney <wayne@xronlinux.com> + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + + + + License: GPL-3.0 + + + Qt::AlignHCenter|Qt::AlignVCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/kwin/src/xrdriveripc/xrdriveripc.cpp b/kwin/src/xrdriveripc/xrdriveripc.cpp index bad7835..168043d 100644 --- a/kwin/src/xrdriveripc/xrdriveripc.cpp +++ b/kwin/src/xrdriveripc/xrdriveripc.cpp @@ -27,14 +27,6 @@ XRDriverIPC &XRDriverIPC::instance() { return inst; } -std::string XRDriverIPC::string(const XRDict &dict, const std::string &key) { - auto it = dict.find(key); - if (it != dict.end() && std::holds_alternative(it->second)) { - return std::get(it->second); - } - return {}; -} - std::string XRDriverIPC::configHome() const { QString configHome = QString::fromUtf8(qgetenv("XDG_CONFIG_HOME")); if (configHome.isEmpty()) { @@ -75,47 +67,24 @@ QByteArray XRDriverIPC::invokePython(const QString &method, return proc.readAllStandardOutput().trimmed(); } -static XRDict jsonToXRDict(const QJsonObject &obj) { - XRDict out; - for (auto it = obj.begin(); it != obj.end(); ++it) { - const QString &k = it.key(); - const QJsonValue &v = it.value(); - if (v.isBool()) out[k.toStdString()] = v.toBool(); - else if (v.isDouble() && std::floor(v.toDouble()) == v.toDouble()) - out[k.toStdString()] = (int)v.toDouble(); - else if (v.isDouble()) out[k.toStdString()] = v.toDouble(); - else if (v.isString()) out[k.toStdString()] = v.toString().toStdString(); - else out[k.toStdString()] = std::monostate{}; - } - return out; -} - -std::optional XRDriverIPC::retrieveConfig() { +std::optional XRDriverIPC::retrieveConfig() { QByteArray out = invokePython(QStringLiteral("retrieve_config"), {}, QStringLiteral("1")); if (out.isEmpty()) return std::nullopt; QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; - return jsonToXRDict(doc.object()); + return doc.object(); } -std::optional XRDriverIPC::retrieveDriverState() { +std::optional XRDriverIPC::retrieveDriverState() { QByteArray out = invokePython(QStringLiteral("retrieve_driver_state"), {}, {}); if (out.isEmpty()) return std::nullopt; QJsonParseError err; auto doc = QJsonDocument::fromJson(out, &err); if (err.error != QJsonParseError::NoError || !doc.isObject()) return std::nullopt; - return jsonToXRDict(doc.object()); + return doc.object(); } -bool XRDriverIPC::writeConfig(const XRDict &configUpdate) { - QJsonObject obj; - for (const auto &kv : configUpdate) { - const std::string &k = kv.first; const XRValue &v = kv.second; - if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), std::get(v)); - else if (std::holds_alternative(v)) obj.insert(QString::fromStdString(k), QString::fromStdString(std::get(v))); - } - QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); +bool XRDriverIPC::writeConfig(const QJsonObject &configUpdate) { + QByteArray payload = QJsonDocument(configUpdate).toJson(QJsonDocument::Compact); QByteArray out = invokePython(QStringLiteral("write_config"), payload, {}); return !out.isEmpty(); } @@ -130,13 +99,13 @@ bool XRDriverIPC::writeControlFlags(const std::map &flags) { bool XRDriverIPC::requestToken(const std::string &email) { QByteArray out = invokePython(QStringLiteral("request_token"), {}, QString::fromStdString(email)); if (out.isEmpty()) return false; - auto value = QJsonValue(QString::fromStdString(out.toStdString())); - return value.isBool() ? value.toBool() : false; + QString result = QString::fromUtf8(out).trimmed().toLower(); + return result == QStringLiteral("true"); } bool XRDriverIPC::verifyToken(const std::string &token) { QByteArray out = invokePython(QStringLiteral("verify_token"), {}, QString::fromStdString(token)); if (out.isEmpty()) return false; - auto value = QJsonValue(QString::fromStdString(out.toStdString())); - return value.isBool() ? value.toBool() : false; + QString result = QString::fromUtf8(out).trimmed().toLower(); + return result == QStringLiteral("true"); } diff --git a/kwin/src/xrdriveripc/xrdriveripc.h b/kwin/src/xrdriveripc/xrdriveripc.h index ff6d654..ee6ca81 100644 --- a/kwin/src/xrdriveripc/xrdriveripc.h +++ b/kwin/src/xrdriveripc/xrdriveripc.h @@ -1,13 +1,10 @@ // C++ bridge now invoking xrdriveripc via external python process #pragma once -#include -#include -#include -#include -#include #include #include +#include +#include // Export header generated by CMake (GenerateExportHeader) #ifdef __has_include @@ -78,18 +75,13 @@ namespace XRConfigEntry { inline constexpr const char *Debug = "debug"; } -// Simple variant type for config/state key values we care about -using XRValue = std::variant; -using XRDict = std::map; - class XR_DRIVER_IPC_EXPORT XRDriverIPC { public: static XRDriverIPC &instance(); - static std::string string(const XRDict &dict, const std::string &key); - std::optional retrieveConfig(); - std::optional retrieveDriverState(); - bool writeConfig(const XRDict &configUpdate); + std::optional retrieveConfig(); + std::optional retrieveDriverState(); + bool writeConfig(const QJsonObject &configUpdate); bool writeControlFlags(const std::map &flags); bool requestToken(const std::string &email); bool verifyToken(const std::string &token); diff --git a/kwin/src/xrdriveripc/xrdriveripc_runner.py b/kwin/src/xrdriveripc/xrdriveripc_runner.py index 748bc70..3a47250 100644 --- a/kwin/src/xrdriveripc/xrdriveripc_runner.py +++ b/kwin/src/xrdriveripc/xrdriveripc_runner.py @@ -13,6 +13,13 @@ import os import sys import traceback +class Logger: + def info(self, *args, **kwargs): + pass + + def error(self, *args, **kwargs): + pass + def main() -> int: # Ensure the current directory (where xrdriveripc.py lives) is in sys.path @@ -32,7 +39,7 @@ def main() -> int: return 2 config_home = os.environ.get("BREEZY_CONFIG_HOME") - inst = xrdriveripc.XRDriverIPC(config_home=config_home) + inst = xrdriveripc.XRDriverIPC(logger=Logger(), config_home=config_home) arg = os.environ.get("BREEZY_ARG") payload_raw = os.environ.get("BREEZY_PAYLOAD") diff --git a/ui/src/licensefeaturerow.py b/ui/src/licensefeaturerow.py index 68ed4ac..02f2c52 100644 --- a/ui/src/licensefeaturerow.py +++ b/ui/src/licensefeaturerow.py @@ -26,12 +26,9 @@ class LicenseFeatureRow(Adw.ActionRow): self.set_subtitle(f"{status}{details}") def _feature_name(self, feature): - print(f"Translating feature: {feature}") - print(f"_ is: {_}") feature_names = { - 'sbs': lambda: gettext.gettext('Side-by-side mode (gaming)'), + 'sbs': lambda: _('Side-by-side mode (gaming)'), 'smooth_follow': lambda: _('Smooth Follow (gaming)'), 'productivity_basic': lambda: _('Breezy Desktop (productivity)') } - print(f"Translated string: {feature_names[feature]()}") return feature_names[feature]() \ No newline at end of file