Merge lp:~mikemc/unity-scope-click/startdownload-error-handling into lp:unity-scope-click

Proposed by Mike McCracken
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 153
Merged at revision: 162
Proposed branch: lp:~mikemc/unity-scope-click/startdownload-error-handling
Merge into: lp:unity-scope-click
Prerequisite: lp:~diegosarmentero/unity-scope-click/fix-progress
Diff against target: 413 lines (+147/-38)
7 files modified
scope/click/download-manager.cpp (+23/-8)
scope/click/download-manager.h (+5/-1)
scope/click/preview.cpp (+51/-11)
scope/click/preview.h (+15/-0)
scope/click/scope.cpp (+13/-2)
scope/tests/download_manager_tool/download_manager_tool.cpp (+9/-2)
scope/tests/test_download_manager.cpp (+31/-14)
To merge this branch: bzr merge lp:~mikemc/unity-scope-click/startdownload-error-handling
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Diego Sarmentero (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+207356@code.launchpad.net

Commit message

- Surface errors in the download and install process via ErrorPreview

Description of the change

- Surface errors in the download and install process via ErrorPreview

Shows user error messages when
- creds are not found
- any error is returned from download manager, including network errors or failed install

Other changes:
- fix tests to account for separate error signals for creds vs. downloading/installing.
- fix a problem in the test matcher for the pkcon command
- use constants instead of strings in some places
- cleanup whitespace

TO TEST:

The download manager tests are currently disabled because they're flaky on ARM.
To run all the tests, which should pass on x86, do:
% GTEST_ALSO_RUN_DISABLED_TESTS=1 U1_DEBUG=1 make test

TO TEST IRL:
using unity-scope-tool or a device, try installing an app with:

- no credentials - you should get a specific error message for this
- no network - you should get a network error or a download/install error, depending on when you cut the net.
- install error - not sure how to trigger this on the device. maybe you could mess with permissions of the click directory temporarily. on desktop, it's easy, since pkcon doesn't work with clicks on desktop, according to the packagekit-plugin-click README:
       http://bazaar.launchpad.net/~click-hackers/click/trunk/view/head:/pk-plugin/README

In each case you should get a serviceable (untranslated, un-designed for wording) error message.

To post a comment you must log in.
153. By Mike McCracken

remove unused boost/optional header

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

+1

review: Approve
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'scope/click/download-manager.cpp'
--- scope/click/download-manager.cpp 2014-02-18 11:26:15 +0000
+++ scope/click/download-manager.cpp 2014-02-20 06:27:03 +0000
@@ -215,7 +215,7 @@
215void click::DownloadManager::handleCredentialsNotFound()215void click::DownloadManager::handleCredentialsNotFound()
216{216{
217 qDebug() << "No credentials were found.";217 qDebug() << "No credentials were found.";
218 emit clickTokenFetchError(QString("No creds found"));218 emit credentialsNotFound();
219}219}
220220
221void click::DownloadManager::handleNetworkFinished()221void click::DownloadManager::handleNetworkFinished()
@@ -295,7 +295,8 @@
295 // TODO, unimplemented. see https://bugs.launchpad.net/ubuntu-download-manager/+bug/1277814295 // TODO, unimplemented. see https://bugs.launchpad.net/ubuntu-download-manager/+bug/1277814
296}296}
297297
298void click::Downloader::startDownload(std::string url, std::string package_name, const std::function<void (std::string)>& callback)298void click::Downloader::startDownload(std::string url, std::string package_name,
299 const std::function<void (std::pair<std::string, click::InstallError >)>& callback)
299{300{
300 qt::core::world::enter_with_task([this, callback, url, package_name] (qt::core::world::Environment& /*env*/)301 qt::core::world::enter_with_task([this, callback, url, package_name] (qt::core::world::Environment& /*env*/)
301 {302 {
@@ -304,16 +305,30 @@
304 QObject::connect(&dm, &click::DownloadManager::downloadStarted,305 QObject::connect(&dm, &click::DownloadManager::downloadStarted,
305 [callback](QString downloadId)306 [callback](QString downloadId)
306 {307 {
307 std::cout << "Download started: " << downloadId.toStdString() << std::endl;308 qDebug() << "Download started, id: " << downloadId;
308 callback(downloadId.toUtf8().data());309 auto ret = std::pair<std::string, click::InstallError>(downloadId.toUtf8().data(),
309 });310 click::InstallError::NoError);
311 callback(ret);
312 });
313
314 QObject::connect(&dm, &click::DownloadManager::credentialsNotFound,
315 [callback]()
316 {
317 qDebug() << "Credentials not found:";
318 auto ret = std::pair<std::string, click::InstallError>(std::string(),
319 click::InstallError::CredentialsError);
320 callback(ret);
321 });
322
310 QObject::connect(&dm, &click::DownloadManager::downloadError,323 QObject::connect(&dm, &click::DownloadManager::downloadError,
311 [](QString errorMessage)324 [callback](QString errorMessage)
312 {325 {
313 // TODO: unclear how to handle errors
314 qDebug() << "Error creating download:" << errorMessage;326 qDebug() << "Error creating download:" << errorMessage;
327 auto ret = std::pair<std::string, click::InstallError>(errorMessage.toStdString(),
328 click::InstallError::DownloadInstallError);
329 callback(ret);
315 });330 });
316 std::cout << "About to call start download" << std::endl;331
317 dm.startDownload(QString::fromStdString(url),332 dm.startDownload(QString::fromStdString(url),
318 QString::fromStdString(package_name));333 QString::fromStdString(package_name));
319 });334 });
320335
=== modified file 'scope/click/download-manager.h'
--- scope/click/download-manager.h 2014-02-18 11:26:15 +0000
+++ scope/click/download-manager.h 2014-02-20 06:27:03 +0000
@@ -67,6 +67,8 @@
67 virtual void fetchClickToken(const QString& downloadUrl);67 virtual void fetchClickToken(const QString& downloadUrl);
6868
69signals:69signals:
70
71 void credentialsNotFound();
70 void downloadStarted(const QString& downloadObjectPath);72 void downloadStarted(const QString& downloadObjectPath);
71 void downloadError(const QString& errorMessage);73 void downloadError(const QString& errorMessage);
72 void clickTokenFetched(const QString& clickToken);74 void clickTokenFetched(const QString& clickToken);
@@ -86,13 +88,15 @@
86 QScopedPointer<Private> impl;88 QScopedPointer<Private> impl;
87};89};
8890
91enum class InstallError {NoError, CredentialsError, DownloadInstallError};
8992
90class Downloader93class Downloader
91{94{
92public:95public:
93 Downloader(const QSharedPointer<click::network::AccessManager>& networkAccessManager);96 Downloader(const QSharedPointer<click::network::AccessManager>& networkAccessManager);
94 void get_download_progress(std::string package_name, const std::function<void (std::string)>& callback);97 void get_download_progress(std::string package_name, const std::function<void (std::string)>& callback);
95 void startDownload(std::string url, std::string package_name, const std::function<void (std::string)>& callback);98 void startDownload(std::string url, std::string package_name,
99 const std::function<void (std::pair<std::string, InstallError>)>& callback);
96private:100private:
97 QSharedPointer<click::network::AccessManager> networkAccessManager;101 QSharedPointer<click::network::AccessManager> networkAccessManager;
98};102};
99103
=== modified file 'scope/click/preview.cpp'
--- scope/click/preview.cpp 2014-02-20 06:27:03 +0000
+++ scope/click/preview.cpp 2014-02-20 06:27:03 +0000
@@ -41,6 +41,8 @@
4141
42#include <QDebug>42#include <QDebug>
4343
44#include <boost/optional.hpp>
45
44#include <sstream>46#include <sstream>
4547
46namespace48namespace
@@ -125,12 +127,14 @@
125 reply->push(widgets);127 reply->push(widgets);
126}128}
127129
128void buildErrorPreview(scopes::PreviewReplyProxy const& reply)130void buildErrorPreview(scopes::PreviewReplyProxy const& reply,
131 const std::string& error_message)
129{132{
130 scopes::PreviewWidgetList widgets;133 scopes::PreviewWidgetList widgets;
131134
132 scopes::PreviewWidget header("hdr", "header");135 scopes::PreviewWidget header("hdr", "header");
133 header.add_attribute("title", scopes::Variant("Error"));136 header.add_attribute("title", scopes::Variant("Error"));
137 header.add_attribute("subtitle", scopes::Variant(error_message));
134 widgets.push_back(header);138 widgets.push_back(header);
135139
136 scopes::PreviewWidget buttons("buttons", "actions");140 scopes::PreviewWidget buttons("buttons", "actions");
@@ -151,6 +155,7 @@
151155
152 scopes::PreviewWidget header("hdr", "header");156 scopes::PreviewWidget header("hdr", "header");
153 header.add_attribute("title", scopes::Variant("Login Error"));157 header.add_attribute("title", scopes::Variant("Login Error"));
158 header.add_attribute("subtitle", scopes::Variant("Please log in to your Ubuntu One account."));
154 widgets.push_back(header);159 widgets.push_back(header);
155160
156 scopes::PreviewWidget buttons("buttons", "actions");161 scopes::PreviewWidget buttons("buttons", "actions");
@@ -312,9 +317,6 @@
312 case Type::UNINSTALLED:317 case Type::UNINSTALLED:
313 buildUninstalledPreview(reply, details);318 buildUninstalledPreview(reply, details);
314 break;319 break;
315 case Type::ERROR:
316 buildErrorPreview(reply);
317 break;
318 case Type::LOGIN:320 case Type::LOGIN:
319 buildLoginErrorPreview(reply);321 buildLoginErrorPreview(reply);
320 break;322 break;
@@ -328,8 +330,12 @@
328 buildPurchasingPreview(reply, details);330 buildPurchasingPreview(reply, details);
329 break;331 break;
330 case Type::DEFAULT:332 case Type::DEFAULT:
333 case Type::ERROR:
334 // Don't showPreview() with errors. always use the error string.
331 default:335 default:
332 buildDefaultPreview(reply, details);336 qDebug() << "reached default preview type, returning internal error preview";
337 buildErrorPreview(reply,
338 std::string("Internal Error, please close and try again."));
333 break;339 break;
334 };340 };
335}341}
@@ -339,6 +345,30 @@
339 this->type = type;345 this->type = type;
340}346}
341347
348
349// ErrorPreview
350
351ErrorPreview::ErrorPreview(const std::string& error_message,
352 const QSharedPointer<click::Index>& index,
353 const unity::scopes::Result &result)
354 : Preview(result.uri(), index, result), error_message(error_message)
355{
356 qDebug() << "in ErrorPreview constructor, error_message is " << QString::fromStdString(error_message);
357}
358
359ErrorPreview::~ErrorPreview()
360{
361
362}
363
364void ErrorPreview::run(const unity::scopes::PreviewReplyProxy &reply)
365{
366 buildErrorPreview(reply, error_message);
367}
368
369
370// InstallPreview
371
342InstallPreview::InstallPreview(const std::string &download_url, const QSharedPointer<Index> &index,372InstallPreview::InstallPreview(const std::string &download_url, const QSharedPointer<Index> &index,
343 const unity::scopes::Result &result,373 const unity::scopes::Result &result,
344 const QSharedPointer<click::network::AccessManager> &nam)374 const QSharedPointer<click::network::AccessManager> &nam)
@@ -356,12 +386,22 @@
356void InstallPreview::run(const unity::scopes::PreviewReplyProxy &reply)386void InstallPreview::run(const unity::scopes::PreviewReplyProxy &reply)
357{387{
358 qDebug() << "about to call startDownload in run()";388 qDebug() << "about to call startDownload in run()";
359 downloader->startDownload(download_url, result["name"].get_string(),[this, reply](std::string obj_path) {389 downloader->startDownload(download_url, result["name"].get_string(),[this, reply](std::pair<std::string, click::InstallError> pair) {
360 qDebug() << "got object path: " << QString::fromStdString(obj_path);390
361 index->get_details(result["name"].get_string(), [this, reply, obj_path](const click::PackageDetails& details)391 auto error = pair.second;
362 {392
363 buildInstallingPreview(reply, details, obj_path);393 if (error == InstallError::NoError) {
364 });394 auto obj_path = pair.first;
395 qDebug() << "got object path: " << QString::fromStdString(obj_path);
396 index->get_details(result["name"].get_string(), [this, reply, obj_path](const click::PackageDetails& details)
397 {
398 buildInstallingPreview(reply, details, obj_path);
399 });
400 } else if (error == InstallError::CredentialsError) {
401 buildLoginErrorPreview(reply);
402 } else {
403 buildErrorPreview(reply, pair.first);
404 }
365 });405 });
366 qDebug() << "after startDownload in run()";406 qDebug() << "after startDownload in run()";
367}407}
368408
=== modified file 'scope/click/preview.h'
--- scope/click/preview.h 2014-02-18 12:44:49 +0000
+++ scope/click/preview.h 2014-02-20 06:27:03 +0000
@@ -108,6 +108,21 @@
108 const click::PackageDetails& details);108 const click::PackageDetails& details);
109};109};
110110
111class ErrorPreview : public Preview
112{
113protected:
114 std::string error_message;
115public:
116 ErrorPreview(const std::string& error_message,
117 const QSharedPointer<click::Index>& index,
118 const unity::scopes::Result& result);
119
120 virtual ~ErrorPreview();
121
122 void run(unity::scopes::PreviewReplyProxy const& reply) override;
123
124};
125
111class InstallPreview : public Preview126class InstallPreview : public Preview
112{127{
113protected:128protected:
114129
=== modified file 'scope/click/scope.cpp'
--- scope/click/scope.cpp 2014-02-20 06:27:03 +0000
+++ scope/click/scope.cpp 2014-02-20 06:27:03 +0000
@@ -101,10 +101,16 @@
101101
102 if (metadata.scope_data().which() != scopes::Variant::Type::Null) {102 if (metadata.scope_data().which() != scopes::Variant::Type::Null) {
103 auto metadict = metadata.scope_data().get_dict();103 auto metadict = metadata.scope_data().get_dict();
104 if (metadict.count("download_completed") != 0) {104
105 if(metadict.count(click::Preview::Actions::DOWNLOAD_FAILED) != 0) {
106 return scopes::QueryBase::UPtr{new ErrorPreview(std::string("Download or install failed. Please try again."),
107 index, result)};
108
109 } else if (metadict.count(click::Preview::Actions::DOWNLOAD_COMPLETED) != 0) {
105 Preview* prev = new Preview(result.uri(), index, result);110 Preview* prev = new Preview(result.uri(), index, result);
106 prev->setPreview(click::Preview::Type::INSTALLED);111 prev->setPreview(click::Preview::Type::INSTALLED);
107 return scopes::QueryBase::UPtr{prev};112 return scopes::QueryBase::UPtr{prev};
113
108 } else if (metadict.count("action_id") != 0 &&114 } else if (metadict.count("action_id") != 0 &&
109 metadict.count("download_url") != 0) {115 metadict.count("download_url") != 0) {
110 action_id = metadict["action_id"].get_string();116 action_id = metadict["action_id"].get_string();
@@ -132,8 +138,13 @@
132 activation->setHint("action_id", unity::scopes::Variant(action_id));138 activation->setHint("action_id", unity::scopes::Variant(action_id));
133 qDebug() << "returning ShowPreview";139 qDebug() << "returning ShowPreview";
134 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);140 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
141
142 } else if (action_id == click::Preview::Actions::DOWNLOAD_FAILED) {
143 activation->setHint(click::Preview::Actions::DOWNLOAD_FAILED, unity::scopes::Variant(true));
144 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
145
135 } else if (action_id == click::Preview::Actions::DOWNLOAD_COMPLETED) {146 } else if (action_id == click::Preview::Actions::DOWNLOAD_COMPLETED) {
136 activation->setHint("download_completed", unity::scopes::Variant(true));147 activation->setHint(click::Preview::Actions::DOWNLOAD_COMPLETED, unity::scopes::Variant(true));
137 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);148 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
138 }149 }
139 return scopes::ActivationBase::UPtr(activation);150 return scopes::ActivationBase::UPtr(activation);
140151
=== modified file 'scope/tests/download_manager_tool/download_manager_tool.cpp'
--- scope/tests/download_manager_tool/download_manager_tool.cpp 2014-02-18 07:55:54 +0000
+++ scope/tests/download_manager_tool/download_manager_tool.cpp 2014-02-20 06:27:03 +0000
@@ -35,6 +35,8 @@
3535
36#include <iostream>36#include <iostream>
3737
38#include <boost/optional.hpp>
39
38#include <download_manager_tool.h>40#include <download_manager_tool.h>
3941
40DownloadManagerTool::DownloadManagerTool(QObject *parent):42DownloadManagerTool::DownloadManagerTool(QObject *parent):
@@ -91,8 +93,13 @@
9193
92 QObject::connect(&timer, &QTimer::timeout, [&]() {94 QObject::connect(&timer, &QTimer::timeout, [&]() {
93 downloader.startDownload(std::string(argv[1]), std::string(argv[2]),95 downloader.startDownload(std::string(argv[1]), std::string(argv[2]),
94 [&a] (std::string download_id){96 [&a] (std::pair<std::string, click::InstallError> arg){
95 std::cout << " Success, got download ID:" << download_id << std::endl;97 auto error = arg.second;
98 if (error == click::InstallError::NoError) {
99 std::cout << " Success, got download ID:" << arg.first << std::endl;
100 } else {
101 std::cout << " Error:" << arg.first << std::endl;
102 }
96 a.quit();103 a.quit();
97 });104 });
98 105
99106
=== modified file 'scope/tests/test_download_manager.cpp'
--- scope/tests/test_download_manager.cpp 2014-02-17 21:32:33 +0000
+++ scope/tests/test_download_manager.cpp 2014-02-20 06:27:03 +0000
@@ -184,11 +184,11 @@
184 });184 });
185 signalTimer.start(10);185 signalTimer.start(10);
186 }186 }
187
188};187};
189188
190struct DownloadManagerMockClient189struct DownloadManagerMockClient
191{190{
191 MOCK_METHOD0(onCredentialsNotFoundEmitted, void());
192 MOCK_METHOD1(onClickTokenFetchedEmitted, void(QString clickToken));192 MOCK_METHOD1(onClickTokenFetchedEmitted, void(QString clickToken));
193 MOCK_METHOD1(onClickTokenFetchErrorEmitted, void(QString errorMessage));193 MOCK_METHOD1(onClickTokenFetchErrorEmitted, void(QString errorMessage));
194 MOCK_METHOD1(onDownloadStartedEmitted, void(QString id));194 MOCK_METHOD1(onDownloadStartedEmitted, void(QString id));
@@ -256,6 +256,12 @@
256256
257 DownloadManagerMockClient mockDownloadManagerClient;257 DownloadManagerMockClient mockDownloadManagerClient;
258258
259 QObject::connect(&dm, &click::DownloadManager::credentialsNotFound,
260 [&mockDownloadManagerClient]()
261 {
262 mockDownloadManagerClient.onCredentialsNotFoundEmitted();
263 });
264
259 QObject::connect(&dm, &click::DownloadManager::clickTokenFetchError,265 QObject::connect(&dm, &click::DownloadManager::clickTokenFetchError,
260 [&mockDownloadManagerClient](const QString& error)266 [&mockDownloadManagerClient](const QString& error)
261 {267 {
@@ -282,12 +288,23 @@
282288
283 } else {289 } else {
284290
285 EXPECT_CALL(mockDownloadManagerClient, onClickTokenFetchErrorEmitted(_))291 if (p.credsFound) {
286 .Times(1)292
287 .WillOnce(293 EXPECT_CALL(mockDownloadManagerClient, onClickTokenFetchErrorEmitted(_))
288 InvokeWithoutArgs(294 .Times(1)
289 this,295 .WillOnce(
290 &DISABLED_DownloadManagerCredsNetworkTest::Quit));296 InvokeWithoutArgs(
297 this,
298 &DISABLED_DownloadManagerCredsNetworkTest::Quit));
299 } else {
300
301 EXPECT_CALL(mockDownloadManagerClient, onCredentialsNotFoundEmitted())
302 .Times(1)
303 .WillOnce(
304 InvokeWithoutArgs(
305 this,
306 &DISABLED_DownloadManagerCredsNetworkTest::Quit));
307 }
291308
292 EXPECT_CALL(mockDownloadManagerClient, onClickTokenFetchedEmitted(_)).Times(0);309 EXPECT_CALL(mockDownloadManagerClient, onClickTokenFetchedEmitted(_)).Times(0);
293310
@@ -326,12 +343,12 @@
326{343{
327 auto commandList = arg.getMetadata()["post-download-command"].toStringList();344 auto commandList = arg.getMetadata()["post-download-command"].toStringList();
328 return arg.getUrl() == TEST_URL345 return arg.getUrl() == TEST_URL
329 && arg.getHash() == "" 346 && arg.getHash() == ""
330 && arg.getAlgorithm() == ""347 && arg.getAlgorithm() == ""
331 && arg.getMetadata()["app_id"] == QVariant(TEST_APP_ID)348 && arg.getMetadata()["app_id"] == QVariant(TEST_APP_ID)
332 && commandList[0] == "/bin/sh"349 && commandList[0] == "/bin/sh"
333 && commandList[1] == "-c"350 && commandList[1] == "-c"
334 && commandList[3] == "$files"351 && commandList[3] == "$file"
335 && arg.getHeaders()["X-Click-Token"] == TEST_CLICK_TOKEN_VALUE;352 && arg.getHeaders()["X-Click-Token"] == TEST_CLICK_TOKEN_VALUE;
336}353}
337354
@@ -385,14 +402,14 @@
385 });402 });
386 }403 }
387404
388 EXPECT_CALL(*mockSystemDownloadManager, 405 EXPECT_CALL(*mockSystemDownloadManager,
389 createDownload(DownloadStructIsValid())).Times(1).WillOnce(InvokeWithoutArgs(downloadCreatedSignalFunc));406 createDownload(DownloadStructIsValid())).Times(1).WillOnce(InvokeWithoutArgs(downloadCreatedSignalFunc));
390 } 407 }
391408
392 EXPECT_CALL(*mockCredentialsService, getCredentials())409 EXPECT_CALL(*mockCredentialsService, getCredentials())
393 .Times(1).WillOnce(InvokeWithoutArgs(clickTokenSignalFunc));410 .Times(1).WillOnce(InvokeWithoutArgs(clickTokenSignalFunc));
394411
395 412
396 DownloadManagerMockClient mockDownloadManagerClient;413 DownloadManagerMockClient mockDownloadManagerClient;
397414
398 QObject::connect(&dm, &click::DownloadManager::downloadError,415 QObject::connect(&dm, &click::DownloadManager::downloadError,
@@ -418,7 +435,7 @@
418 &DISABLED_DownloadManagerStartDownloadTest::Quit));435 &DISABLED_DownloadManagerStartDownloadTest::Quit));
419436
420 EXPECT_CALL(mockDownloadManagerClient, onDownloadErrorEmitted(_)).Times(0);437 EXPECT_CALL(mockDownloadManagerClient, onDownloadErrorEmitted(_)).Times(0);
421 438
422 // when https://bugs.launchpad.net/ubuntu-download-manager/+bug/1278789439 // when https://bugs.launchpad.net/ubuntu-download-manager/+bug/1278789
423 // is fixed, we can add this assertion:440 // is fixed, we can add this assertion:
424 // EXPECT_CALL(successfulDownload, start()).Times(1);441 // EXPECT_CALL(successfulDownload, start()).Times(1);
@@ -435,7 +452,7 @@
435 EXPECT_CALL(mockDownloadManagerClient, onDownloadStartedEmitted(_)).Times(0);452 EXPECT_CALL(mockDownloadManagerClient, onDownloadStartedEmitted(_)).Times(0);
436453
437 }454 }
438 455
439 QTimer timer;456 QTimer timer;
440 timer.setSingleShot(true);457 timer.setSingleShot(true);
441 QObject::connect(&timer, &QTimer::timeout, [&dm]() {458 QObject::connect(&timer, &QTimer::timeout, [&dm]() {

Subscribers

People subscribed via source and target branches

to all changes: