1212namespace {
1313
1414Q_DECLARE_LOGGING_CATEGORY (plugin)
15- Q_LOGGING_CATEGORY (plugin, " copyq.plugin.itemtests " )
15+ Q_LOGGING_CATEGORY (plugin, " copyq.keys " )
1616
17- QString objectAddress (QObject *object )
17+ QString widgetAddress (QWidget *widget )
1818{
19- if (!object )
19+ if (!widget )
2020 return QStringLiteral (" <null>" );
2121
2222 QString result;
23- QObject *current = object ;
23+ QWidget *current = widget ;
2424 while (current) {
2525 const QString className = current->metaObject ()->className ();
2626 const QString objectName = current->objectName ();
@@ -33,14 +33,16 @@ QString objectAddress(QObject *object)
3333 if (!result.isEmpty ())
3434 result.append (' <' );
3535 result.append (name);
36- const QString text = current->property (" text" ).toString ()
36+ QString text = current->property (" text" ).toString ()
3737 .remove (' &' )
3838 // Remove HTML tags
3939 .remove (QRegularExpression (QStringLiteral (" </?[^>]*>" )));
40+ if ( text.isEmpty () && current->isWindow () )
41+ text = current->windowTitle ();
4042 if ( !text.isEmpty () )
4143 result.append (QStringLiteral (" '%1'" ).arg (text));
4244 }
43- current = current->parent ();
45+ current = current->parentWidget ();
4446 }
4547 return result;
4648}
@@ -75,7 +77,7 @@ QWidget *findWidgetWithProperties(const QString &properties, QWidget *parent)
7577 const QStringList props = name.split (' |' , Qt::SkipEmptyParts);
7678 for (QWidget *child : parent->findChildren <QWidget*>()) {
7779 if (child->isVisible () && matchesProperties (child, props)) {
78- qCDebug (plugin) << " Found target:" << objectAddress (child);
80+ qCDebug (plugin) << " Found target:" << widgetAddress (child);
7981 return child;
8082 }
8183 }
@@ -84,10 +86,28 @@ QWidget *findWidgetWithProperties(const QString &properties, QWidget *parent)
8486 });
8587}
8688
89+ bool checkEventTarget (
90+ QWidget *target, const QString &keys, const QString &widgetName, const char *event)
91+ {
92+ if (target && target->isVisible ())
93+ return true ;
94+
95+ qCCritical (plugin) << " Failed to send postponed" << event
96+ << keys << " to" << widgetName << " ;"
97+ << (target ? " Target widget is no longer visible" : " Target widget no longer exists" );
98+ return false ;
99+ }
100+
87101} // namespace
88102
89103class KeyClicker final : public QObject {
90104public:
105+ enum Status {
106+ Pending,
107+ Success,
108+ Failed
109+ };
110+
91111 KeyClicker (QObject *parent)
92112 : QObject(parent)
93113 {
@@ -106,10 +126,10 @@ class KeyClicker final : public QObject {
106126 if (retry > 0 )
107127 sendKeyClicks (expectedWidgetName, keys, delay + 100 , retry - 1 );
108128 else
109- keyClicksFailed (expectedWidgetName );
129+ keyClicksFailed ();
110130 }
111131
112- void keyClicksFailed (const QRegularExpression &expectedWidgetName )
132+ void keyClicksFailed ()
113133 {
114134 qCCritical (plugin) << " Failed to send key press to target widget" ;
115135
@@ -124,21 +144,21 @@ class KeyClicker final : public QObject {
124144 qCCritical (plugin) << " App is INACTIVE! State:" << state;
125145
126146 qCCritical (plugin).noquote ().nospace ()
127- << " Expected: /" + expectedWidgetName .pattern () + " /"
128- << " \n Actual: " + objectAddress (actual)
129- << " \n Popup: " + objectAddress (popup)
130- << " \n Widget: " + objectAddress (widget)
131- << " \n Window: " + objectAddress (window)
132- << " \n Modal: " + objectAddress (modal);
133-
134- m_failed = true ;
147+ << " Expected: /" + m_expectedWidgetName .pattern () + " /"
148+ << " \n Actual: " + widgetAddress (actual)
149+ << " \n Popup: " + widgetAddress (popup)
150+ << " \n Widget: " + widgetAddress (widget)
151+ << " \n Window: " + widgetAddress (window)
152+ << " \n Modal: " + widgetAddress (modal);
153+
154+ m_status = Failed ;
135155 }
136156
137- void keyClicks (const QRegularExpression &expectedWidgetName, const QString &keys, int delay, int retry)
157+ void keyClicks (const QString &keys, int delay, int retry)
138158 {
139159 auto widget = keyClicksTarget ();
140160 if (!widget) {
141- keyClicksRetry (expectedWidgetName , keys, delay, retry);
161+ keyClicksRetry (m_expectedWidgetName , keys, delay, retry);
142162 return ;
143163 }
144164
@@ -147,26 +167,27 @@ class KeyClicker final : public QObject {
147167 if (qApp->applicationState () != Qt::ApplicationActive && m_wnd->isVisible ()) {
148168 qCDebug (plugin) << " Re-activating the main window (macOS)" ;
149169 m_wnd->activateWindow ();
170+ m_wnd->raise ();
150171 }
151172#endif
152173
153174 if (qApp->applicationState () != Qt::ApplicationActive) {
154175 qCDebug (plugin) << " Waiting for application to become active" ;
155- keyClicksRetry (expectedWidgetName , keys, delay, retry);
176+ keyClicksRetry (m_expectedWidgetName , keys, delay, retry);
156177 return ;
157178 }
158179
159- const QString widgetName = objectAddress (widget);
160- if ( !expectedWidgetName .pattern ().isEmpty ()
161- && !expectedWidgetName .match (widgetName).hasMatch () )
180+ const QString widgetName = widgetAddress (widget);
181+ if ( !m_expectedWidgetName .pattern ().isEmpty ()
182+ && !m_expectedWidgetName .match (widgetName).hasMatch () )
162183 {
163- keyClicksRetry (expectedWidgetName , keys, delay, retry);
184+ keyClicksRetry (m_expectedWidgetName , keys, delay, retry);
164185 return ;
165186 }
166187
167188 // Only verified focused widget.
168189 if ( keys.isEmpty () ) {
169- m_succeeded = true ;
190+ m_status = Success ;
170191 return ;
171192 }
172193
@@ -175,18 +196,14 @@ class KeyClicker final : public QObject {
175196 if ( qobject_cast<QCheckBox*>(widget) )
176197 QTest::qWait (100 );
177198
178- qCDebug (plugin) << " Sending event " << keys << " to" << widgetName;
199+ qCDebug (plugin) << " Sending" << keys << " to" << widgetName;
179200
180201 static const auto keyClicksPrefix = QLatin1String (" :" );
181202 static const auto mousePrefix = QLatin1String (" mouse|" );
182203 static const auto dragPrefix = QLatin1String (" isDraggingFrom|" );
183204 if ( keys.startsWith (keyClicksPrefix) ) {
184205 const auto text = keys.mid (keyClicksPrefix.size ());
185-
186- QTest::keyClicks (widget, text, Qt::NoModifier, 0 );
187-
188- // Increment key clicks sequence number after typing all the text.
189- m_succeeded = true ;
206+ QTest::keyClicks (widget, text, Qt::NoModifier);
190207 } else if ( keys.startsWith (mousePrefix) ) {
191208 const QString action = keys.section (' |' , 1 , 1 );
192209 const QString properties = keys.section (' |' , 2 );
@@ -199,24 +216,18 @@ class KeyClicker final : public QObject {
199216 if ( !validActions.contains (action) ) {
200217 qCCritical (plugin) << " Failed to match mouse action:" << keys;
201218 qCCritical (plugin) << " Valid mouse actions are:" << validActions;
202- m_failed = true ;
219+ m_status = Failed ;
203220 return ;
204221 }
205222 QPointer<QWidget> source = findWidgetWithProperties (properties, m_wnd);
206223 if (!source) {
207- m_failed = true ;
224+ m_status = Failed ;
208225 return ;
209226 }
210227 // Don't block while processing the events.
211228 runAfterInterval (delay, [=](){
212- if (!source) {
213- qCCritical (plugin) << " Target widget was destroyed" ;
229+ if (!checkEventTarget (source, keys, widgetName, " mouse" ))
214230 return ;
215- }
216- if (!source->isVisible ()) {
217- qCCritical (plugin) << " Target widget is no longer visible" ;
218- return ;
219- }
220231
221232 // Send the event to window instead - it works better.
222233 QWidget *windowWidget = source->window ();
@@ -239,81 +250,69 @@ class KeyClicker final : public QObject {
239250 QTest::mouseMove (source, source->rect ().bottomRight ());
240251 }
241252 });
242- m_succeeded = true ;
243253 } else if ( keys.startsWith (dragPrefix) ) {
244254 const QObject *drag = m_wnd->findChild <QDrag*>();
245255 if (!drag) {
246256 qCCritical (plugin) << " QDrag not started" ;
247- m_failed = true ;
257+ m_status = Failed ;
248258 return ;
249259 }
250- qCDebug (plugin) << " QDrag started with parent:" << objectAddress ( drag->parent ());
260+ qCDebug (plugin) << " QDrag started with parent:" << widgetAddress (qobject_cast<QWidget*>( drag->parent () ));
251261 const QString properties = keys.section (' |' , 1 );
252262 QWidget* source = findWidgetWithProperties (properties, m_wnd);
253263 if (!source) {
254- m_failed = true ;
264+ m_status = Failed ;
255265 return ;
256266 }
257267 if (drag->parent () != source) {
258268 qCCritical (plugin) << " Unexpected QDrag parent; Expected:" << properties
259- << " ; Actual:" << objectAddress ( drag->parent ());
260- m_failed = true ;
269+ << " ; Actual:" << widgetAddress (qobject_cast<QWidget*>( drag->parent () ));
270+ m_status = Failed ;
261271 return ;
262272 }
263- m_succeeded = true ;
264273 } else {
265274 const QKeySequence shortcut (keys, QKeySequence::PortableText);
266-
267275 if ( shortcut.isEmpty () ) {
268276 qCCritical (plugin) << " Failed to parse shortcut" << keys;
269- m_failed = true ;
277+ m_status = Failed ;
270278 return ;
271279 }
272280
273- // Increment key clicks sequence number before opening any modal dialogs.
274- m_succeeded = true ;
275-
276281 const auto key = static_cast <uint>(shortcut[0 ]);
277- const bool postpone = QApplication::activeModalWidget () != nullptr ;
278- if (postpone) {
279- qCDebug (plugin) << " Postponing event due to a modal window" ;
280- // WORKAROUND: Avoid sending release event to a destroyed widget.
281- QPointer<QWidget> target (widget);
282- QTest::keyPress (
283- widget,
284- Qt::Key (key & ~Qt::KeyboardModifierMask),
285- Qt::KeyboardModifiers (key & Qt::KeyboardModifierMask),
286- 1 );
287- QTimer::singleShot (2 , m_wnd, [=]() {
288- if (!target)
289- return ;
290- QTest::keyRelease (
291- target,
292- Qt::Key (key & ~Qt::KeyboardModifierMask),
293- Qt::KeyboardModifiers (key & Qt::KeyboardModifierMask) );
294- });
295- } else {
282+ const QPointer<QWidget> target = widget;
283+ // Avoid blocking on modal dialogs
284+ runAfterInterval (0 , [=](){
285+ if (!target || !target->isVisible ()) {
286+ qCCritical (plugin) << " Target no longer valid:" << widgetName;
287+ return ;
288+ }
296289 QTest::keyClick (
297- widget ,
290+ target ,
298291 Qt::Key (key & ~Qt::KeyboardModifierMask),
299- Qt::KeyboardModifiers (key & Qt::KeyboardModifierMask) );
300- }
292+ Qt::KeyboardModifiers (key & Qt::KeyboardModifierMask));
293+ });
301294 }
302295
303- qCDebug (plugin) << " Event" << keys << " sent to" << widgetName;
296+ m_status = Success;
297+ qCDebug (plugin) << " Sent" << keys << " to" << widgetName;
304298 }
305299
306300 void sendKeyClicks (const QRegularExpression &expectedWidgetName, const QString &keys, int delay, int retry)
307301 {
308- m_succeeded = false ;
309- m_failed = false ;
302+ m_status = Pending ;
303+ m_expectedWidgetName = expectedWidgetName ;
310304
311305 // Don't stop when modal window is open.
312- runAfterInterval (delay, [=](){ keyClicks (expectedWidgetName, keys, delay, retry); });
306+ runAfterInterval (delay, [=](){ keyClicks (keys, delay, retry); });
313307 }
314308
315- bool succeeded () const { return m_succeeded; }
316- bool failed () const { return m_failed; }
309+ int status (bool forceRetrieve) {
310+ if (m_status == Pending && forceRetrieve) {
311+ keyClicksFailed ();
312+ return Failed;
313+ }
314+ return m_status;
315+ }
317316
318317private:
319318 template <typename Callable>
@@ -352,8 +351,8 @@ class KeyClicker final : public QObject {
352351 }
353352
354353 QWidget *m_wnd = nullptr ;
355- bool m_succeeded = true ;
356- bool m_failed = false ;
354+ Status m_status = Success ;
355+ QRegularExpression m_expectedWidgetName ;
357356};
358357
359358void ItemTestsScriptable::keys ()
@@ -375,7 +374,17 @@ void ItemTestsScriptable::keys()
375374 QString expectedWidgetName;
376375
377376 const auto focusPrefix = QLatin1String (" focus:" );
377+ bool interrupted = false ;
378378 for (const auto &arg : currentArguments ()) {
379+ if (interrupted) {
380+ const auto message =
381+ " Client was interrupted."
382+ " This is allowed to happen only when processing the last key event." ;
383+ qCCritical (plugin) << message;
384+ throwError (message);
385+ return ;
386+ }
387+
379388 const QString keys = arg.toString ();
380389
381390 if (keys.startsWith (focusPrefix)) {
@@ -384,18 +393,30 @@ void ItemTestsScriptable::keys()
384393 } else {
385394 call (" sleep" , {wait});
386395 call (" callPlugin" , {" itemtests" , " sendKeys" , expectedWidgetName, keys, delay});
396+ QTest::qWait (qMax (5 , delay));
387397 }
388398
389399 // Make sure all keys are send (shortcuts are postponed because they can be blocked by modal windows).
390- for (;;) {
391- if ( call (" callPlugin" , {" itemtests" , " sendKeysSucceeded" }).toBool () ) {
400+ for (int i = 1 ; ; ++i) {
401+ const QVariant result = call (" callPlugin" , {" itemtests" , " sendKeysStatus" , i > 15 });
402+ bool ok = false ;
403+ const int status = result.toInt (&ok);
404+ if (!ok) {
405+ qCDebug (plugin) << " Client interrupted, got status:" << result;
406+ interrupted = true ;
392407 break ;
393408 }
394409
395- if ( call (" callPlugin" , {" itemtests" , " sendKeysFailed" }).toBool () ) {
410+ if (status == KeyClicker::Success)
411+ break ;
412+
413+ if (status == KeyClicker::Failed) {
396414 throwError (" Failed to send key presses" );
397415 return ;
398416 }
417+
418+ const int waitMs = 8 * i * i;
419+ QTest::qWait (qMin (1000 , waitMs));
399420 }
400421 }
401422}
@@ -413,7 +434,6 @@ QVariant ItemTestsLoader::scriptCallback(const QVariantList &arguments)
413434 const QString expectedWidgetName = arguments.value (1 ).toString ();
414435 const QString keys = arguments.value (2 ).toString ();
415436 const int delay = arguments.value (3 ).toInt ();
416- Q_ASSERT ( keyClicker ()->succeeded () || keyClicker ()->failed () );
417437 const QRegularExpression re = QRegularExpression (
418438 QString (expectedWidgetName)
419439 .replace (QLatin1String (" <" ), QLatin1String (" .*<.*" ))
@@ -422,11 +442,8 @@ QVariant ItemTestsLoader::scriptCallback(const QVariantList &arguments)
422442 return {};
423443 }
424444
425- if (cmd == " sendKeysSucceeded" )
426- return keyClicker ()->succeeded ();
427-
428- if (cmd == " sendKeysFailed" )
429- return keyClicker ()->failed ();
445+ if (cmd == " sendKeysStatus" )
446+ return keyClicker ()->status (arguments.value (1 ).toBool ());
430447
431448 return QStringLiteral (" Unexpected command: %1" ).arg (cmd);
432449}
0 commit comments