diff --git a/jami-qt.pro b/jami-qt.pro index 15cbafc36c5da6939e80ae3baee8401fc9974949..69bffcb451fe772a546a01199a4a8f7ba08dcb85 100644 --- a/jami-qt.pro +++ b/jami-qt.pro @@ -118,9 +118,11 @@ unix { # unix specific HEADERS += \ - src/xrectsel.h + src/dbuserrorhandler.h \ + src/xrectsel.h SOURCES += \ - src/xrectsel.c + src/dbuserrorhandler.cpp \ + src/xrectsel.c } # Input diff --git a/qml.qrc b/qml.qrc index b06ef1cb9cd67a7b42af9923b99b4638bb818b7e..b8e16a65cfeaf2fdc0ac77cdd51bfbcb34d53b70 100644 --- a/qml.qrc +++ b/qml.qrc @@ -137,5 +137,7 @@ <file>src/commoncomponents/PresenceIndicator.qml</file> <file>src/commoncomponents/AvatarImage.qml</file> <file>src/mainview/components/ParticipantOverlayMenu.qml</file> + <file>src/commoncomponents/DaemonReconnectPopup.qml</file> + <file>src/DaemonReconnectWindow.qml</file> </qresource> </RCC> diff --git a/src/DaemonReconnectWindow.qml b/src/DaemonReconnectWindow.qml new file mode 100644 index 0000000000000000000000000000000000000000..ad6fd056423dc12d28fa5ec2edc033e2b63dfcd2 --- /dev/null +++ b/src/DaemonReconnectWindow.qml @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick 2.14 +import QtQuick.Window 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls.Universal 2.14 +import QtGraphicalEffects 1.14 + +// Should not import anything other than this +// to make sure that it is self-dependent +import net.jami.Models 1.0 + +import "commoncomponents" + +ApplicationWindow { + id: root + + property bool connectionFailed: false + property int preferredMargin: 15 + + Universal.theme: Universal.Light + + title: "Jami" + + width: 600 + height: 500 + minimumWidth: 600 + minimumHeight: 500 + + visible: true + + TextMetrics { + id: textMetrics + } + + function getTextBoundingRect(font, text) { + textMetrics.font = font + textMetrics.text = text + + return textMetrics.boundingRect + } + + ResponsiveImage { + id: jamiLogoImage + + anchors.fill: parent + + smooth: true + antialiasing: true + source: "qrc:/images/logo-jami-standard-coul.svg" + } + + Popup { + id: popup + + // center in parent + x: Math.round((root.width - width) / 2) + y: Math.round((root.height - height) / 2) + + modal: true + visible: false + closePolicy: Popup.NoAutoClose + + contentItem: Rectangle { + id: contentRect + + implicitHeight: daemonReconnectPopupColumnLayout.implicitHeight + 50 + + ColumnLayout { + id: daemonReconnectPopupColumnLayout + + anchors.fill: parent + + spacing: 0 + + Text { + id: daemonReconnectPopupTextLabel + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: preferredMargin + + text: connectionFailed ? + qsTr("Could not re-connect to the Jami daemon (dring).\nJami will now quit.") : + qsTr("Trying to reconnect to the Jami daemon (dring)…") + font.pointSize: 11 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Component.onCompleted: { + contentRect.implicitWidth = getTextBoundingRect( + font, text).width + 100 + } + } + + AnimatedImage { + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.preferredHeight: 30 + Layout.preferredWidth: 30 + Layout.bottomMargin: preferredMargin + + visible: !connectionFailed + + source: "qrc:/images/jami_rolling_spinner.gif" + + playing: true + paused: false + mipmap: true + smooth: true + fillMode: Image.PreserveAspectFit + } + + Button { + id: btnOk + + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.preferredWidth: 128 + Layout.preferredHeight: 32 + Layout.bottomMargin: preferredMargin + visible: connectionFailed + + property color hoveredColor: "#0e81c5" + property color pressedColor: "#273261" + property color normalColor: "#00aaff" + + contentItem: Item { + Rectangle { + anchors.fill: parent + color: "transparent" + + Text { + id: buttonText + + anchors.centerIn: parent + + width: { + return (parent.width / 2 - 18) * 2 + } + + text: qsTr("Ok") + + color: { + if (btnOk.hovered) + return btnOk.hoveredColor + if (btnOk.checked) + return btnOk.pressedColor + return btnOk.normalColor + } + font: root.font + horizontalAlignment: Text.AlignHCenter + } + } + } + + onClicked: Qt.quit() + + background: Rectangle { + id: backgroundRect + anchors.fill: parent + color: "transparent" + border.color: { + if (btnOk.hovered) + return btnOk.hoveredColor + if (btnOk.checked) + return btnOk.pressedColor + return btnOk.normalColor + } + radius: 4 + } + } + } + } + } + + Connections { + target: DBusErrorHandler + + function onShowDaemonReconnectPopup(visible) { + if (visible) + popup.open() + else { + popup.close() + Qt.quit() + } + } + + function onDaemonReconnectFailed() { + root.connectionFailed = true + } + } + + overlay.modal: ColorOverlay { + source: root.contentItem + color: "transparent" + + // Color animation for overlay when pop up is shown. + ColorAnimation on color { + to: Qt.rgba(0, 0, 0, 0.33) + duration: 500 + } + } + + Component.onCompleted: { + DBusErrorHandler.setActive(true) + + x = Screen.width / 2 - width / 2 + y = Screen.height / 2 - height / 2 + } +} diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml index 32319ce29b5914eafb9aaf8f63f9075247e988df..d92f9972bf661f920692c740a28948242a188f2d 100644 --- a/src/MainApplicationWindow.qml +++ b/src/MainApplicationWindow.qml @@ -116,6 +116,9 @@ ApplicationWindow { onAccountMigrationFinished: startClient() } + DaemonReconnectPopup { + id: daemonReconnectPopup + } Loader { id: mainApplicationLoader @@ -167,6 +170,26 @@ ApplicationWindow { } } + Connections { + target: { + if (Qt.platform.os !== "windows") + return DBusErrorHandler + return null + } + ignoreUnknownSignals: true + + function onShowDaemonReconnectPopup(visible) { + if (visible) + daemonReconnectPopup.open() + else + daemonReconnectPopup.close() + } + + function onDaemonReconnectFailed() { + daemonReconnectPopup.connectionFailed = true + } + } + onClosing: root.close() onScreenChanged: JamiQmlUtils.mainApplicationScreen = root.screen @@ -176,5 +199,8 @@ ApplicationWindow { startClient() } JamiQmlUtils.mainApplicationScreen = root.screen + + if (Qt.platform.os !== "windows") + DBusErrorHandler.setActive(true) } } diff --git a/src/commoncomponents/DaemonReconnectPopup.qml b/src/commoncomponents/DaemonReconnectPopup.qml new file mode 100644 index 0000000000000000000000000000000000000000..631f9318abeb0068a0e5671790c77cdda49af70b --- /dev/null +++ b/src/commoncomponents/DaemonReconnectPopup.qml @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import net.jami.Models 1.0 +import net.jami.Constants 1.0 + +ModalPopup { + id: root + + property bool connectionFailed: false + property int preferredMargin: 15 + + autoClose: false + + contentItem: Rectangle { + id: contentRect + + implicitHeight: daemonReconnectPopupColumnLayout.implicitHeight + 50 + + color: JamiTheme.secondaryBackgroundColor + + ColumnLayout { + id: daemonReconnectPopupColumnLayout + + anchors.fill: parent + + spacing: 0 + + Text { + id: daemonReconnectPopupTextLabel + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: preferredMargin + + text: connectionFailed ? JamiStrings.reconnectionFailed : + JamiStrings.reconnectDaemon + font.pointSize: JamiTheme.textFontSize + 2 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: JamiTheme.textColor + + Component.onCompleted: { + contentRect.implicitWidth = JamiQmlUtils.getTextBoundingRect( + font, text).width + 100 + } + } + + AnimatedImage { + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.preferredHeight: 30 + Layout.preferredWidth: 30 + Layout.bottomMargin: preferredMargin + + visible: !connectionFailed + + source: "qrc:/images/jami_rolling_spinner.gif"; + + playing: true + paused: false + mipmap: true + smooth: true + fillMode: Image.PreserveAspectFit + } + + MaterialButton { + id: btnOk + + Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom + Layout.preferredWidth: JamiTheme.preferredFieldWidth / 2 + Layout.preferredHeight: JamiTheme.preferredFieldHeight + Layout.bottomMargin: preferredMargin + visible: connectionFailed + + text: qsTr("Ok") + color: JamiTheme.buttonTintedBlue + hoveredColor: JamiTheme.buttonTintedBlueHovered + pressedColor: JamiTheme.buttonTintedBluePressed + outlined: true + + onClicked: Qt.quit() + } + } + } +} diff --git a/src/constant/JamiStrings.qml b/src/constant/JamiStrings.qml index 517fa28b94654de0b4b8cd3766dd1dbc145dfd77..30b13236b32b3ceda9e0482e307c9ea5957407bd 100644 --- a/src/constant/JamiStrings.qml +++ b/src/constant/JamiStrings.qml @@ -406,5 +406,9 @@ Item { property string maximizeParticipant: qsTr("Maximize") property string minimizeParticipant: qsTr("Minimize") property string hangupParticipant: qsTr("Hangup") + + // Daemon reconnection + property string reconnectDaemon: qsTr("Trying to reconnect to the Jami daemon (dring)…") + property string reconnectionFailed: qsTr("Could not re-connect to the Jami daemon (dring).\nJami will now quit.") } diff --git a/src/dbuserrorhandler.cpp b/src/dbuserrorhandler.cpp new file mode 100644 index 0000000000000000000000000000000000000000..33d6bf33177ea59f6c739123e8ec3981a2945254 --- /dev/null +++ b/src/dbuserrorhandler.cpp @@ -0,0 +1,93 @@ +/*! + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "dbuserrorhandler.h" + +#include "api/lrc.h" +#include "globalinstances.h" + +#include <QTimer> + +namespace Interfaces { + +void +DBusErrorHandler::errorCallback() +{ + qDebug() << "Dring has possibly crashed, " + "or has been killed... will wait 2.5 seconds and try to reconnect"; + + emit showDaemonReconnectPopup(true); + + QTimer::singleShot(2500, [this]() { + if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) { + qDebug() << "Could not reconnect to the daemon"; + emit daemonReconnectFailed(); + } else { + static_cast<DBusErrorHandler&>(GlobalInstances::dBusErrorHandler()) + .finishedHandlingError(); + } + }); +} + +void +DBusErrorHandler::setActive(bool active) +{ + handlerActive_ = active; + + if (active) { + if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) + connectionError(QString()); + } +} + +void +DBusErrorHandler::connectionError(const QString& error) +{ + qDebug() << error; + + if (!handlerActive_) + return; + + if (!handlingError) { + handlingError = true; + errorCallback(); + } +} + +void +DBusErrorHandler::invalidInterfaceError(const QString& error) +{ + qDebug() << error; + + if (!handlerActive_) + return; + + if (!handlingError) { + handlingError = true; + errorCallback(); + } +} + +void +DBusErrorHandler::finishedHandlingError() +{ + handlingError = false; + emit showDaemonReconnectPopup(false); +} + +} // namespace Interfaces diff --git a/src/dbuserrorhandler.h b/src/dbuserrorhandler.h new file mode 100644 index 0000000000000000000000000000000000000000..15231bc97020a38236a738779febd3c303b0dafe --- /dev/null +++ b/src/dbuserrorhandler.h @@ -0,0 +1,56 @@ +/*! + * Copyright (C) 2020 by Savoir-faire Linux + * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> + * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <interfaces/dbuserrorhandleri.h> + +namespace Interfaces { + +class DBusErrorHandler : public QObject, public DBusErrorHandlerI +{ + Q_OBJECT +public: + DBusErrorHandler() {}; + ~DBusErrorHandler() {}; + + Q_INVOKABLE void setActive(bool active); + + void connectionError(const QString& error) override; + void invalidInterfaceError(const QString& error) override; + + void finishedHandlingError(); + +signals: + void showDaemonReconnectPopup(bool visible); + void daemonReconnectFailed(); + +private: + void errorCallback(); + + // Keeps track if we're in the process of handling an error already, + // so that we don't keep displaying error dialogs; + // we use an atomic in case the errors come from multiple threads + std::atomic_bool handlingError {false}; + + bool handlerActive_ {false}; +}; + +} // namespace Interfaces +Q_DECLARE_METATYPE(Interfaces::DBusErrorHandler*) diff --git a/src/main.cpp b/src/main.cpp index fe63dbf10a34dee7efbe1b820d9029023eb66b99..51bd99c184376a197b7a8e84cb11b80797463ee1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -80,7 +80,10 @@ main(int argc, char* argv[]) return 0; } - app.init(); + if (!app.init()) { + guard.release(); + return 0; + } /* * Exec the application. diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp index 6b7b365a2283328116b9ad4893b1d544e46668e5..744913ea55d7d5cc2f0450c6dea3da1233493fb9 100644 --- a/src/mainapplication.cpp +++ b/src/mainapplication.cpp @@ -24,6 +24,7 @@ #include "appsettingsmanager.h" #include "connectivitymonitor.h" #include "globalsystemtray.h" +#include "namedirectory.h" #include "qmlregister.h" #include "qrimageprovider.h" #include "tintedbuttonimageprovider.h" @@ -43,6 +44,11 @@ #include <windows.h> #endif +#ifdef Q_OS_UNIX +#include "globalinstances.h" +#include "dbuserrorhandler.h" +#endif + #if defined _MSC_VER && !COMPILE_ONLY #include <gnutls/gnutls.h> #endif @@ -122,7 +128,7 @@ MainApplication::MainApplication(int& argc, char** argv) QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); }); } -void +bool MainApplication::init() { setWindowIcon(QIcon(":images/jami.ico")); @@ -146,6 +152,33 @@ MainApplication::init() gnutls_global_init(); #endif +#ifdef Q_OS_UNIX + GlobalInstances::setDBusErrorHandler(std::make_unique<Interfaces::DBusErrorHandler>()); + auto dBusErrorHandlerQObject = dynamic_cast<QObject*>(&GlobalInstances::dBusErrorHandler()); + qmlRegisterSingletonType<Interfaces::DBusErrorHandler>("net.jami.Models", + 1, + 0, + "DBusErrorHandler", + [dBusErrorHandlerQObject](QQmlEngine* e, + QJSEngine* se) + -> QObject* { + Q_UNUSED(e) + Q_UNUSED(se) + return dBusErrorHandlerQObject; + }); + engine_->setObjectOwnership(dBusErrorHandlerQObject, QQmlEngine::CppOwnership); + + if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) { + engine_->load(QUrl(QStringLiteral("qrc:/src/DaemonReconnectWindow.qml"))); + exec(); + + if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) + return false; + else + engine_.reset(new QQmlApplicationEngine()); + } +#endif + initLrc(results[opts::UPDATEURL].toString(), connectivityMonitor_); connect(connectivityMonitor_, &ConnectivityMonitor::connectivityChanged, [] { @@ -173,6 +206,8 @@ MainApplication::init() initSettings(); initSystray(); initQmlEngine(); + + return true; } void @@ -318,6 +353,12 @@ MainApplication::initQmlEngine() engine_->addImageProvider(QLatin1String("tintedPixmap"), new TintedButtonImageProvider()); engine_->addImageProvider(QLatin1String("avatarImage"), new AvatarImageProvider()); + engine_->setObjectOwnership(&LRCInstance::avModel(), QQmlEngine::CppOwnership); + engine_->setObjectOwnership(&LRCInstance::pluginModel(), QQmlEngine::CppOwnership); + engine_->setObjectOwnership(LRCInstance::getUpdateManager(), QQmlEngine::CppOwnership); + engine_->setObjectOwnership(&LRCInstance::instance(), QQmlEngine::CppOwnership); + engine_->setObjectOwnership(&NameDirectory::instance(), QQmlEngine::CppOwnership); + engine_->load(QUrl(QStringLiteral("qrc:/src/MainApplicationWindow.qml"))); } diff --git a/src/mainapplication.h b/src/mainapplication.h index 4343ec6b33207272d3909bff1fc4b29bd0113079..41269c9b30882ef81d4daea2cd0a3ecf0768b51f 100644 --- a/src/mainapplication.h +++ b/src/mainapplication.h @@ -37,7 +37,7 @@ public: explicit MainApplication(int& argc, char** argv); ~MainApplication() = default; - void init(); + bool init(); private: void loadTranslations(); @@ -51,6 +51,6 @@ private: private: QScopedPointer<QFile> debugFile_; - QQmlApplicationEngine* engine_; + QScopedPointer<QQmlApplicationEngine> engine_; ConnectivityMonitor* connectivityMonitor_; };