@@ -651,6 +651,78 @@ QString tabNameEmptyError()
651
651
return ScriptableProxy::tr (" Tab name cannot be empty!" );
652
652
}
653
653
654
+ #ifdef HAS_TESTS
655
+ QString objectAddress (QObject *object)
656
+ {
657
+ if (!object)
658
+ return QStringLiteral (" <null>" );
659
+
660
+ QString result;
661
+ QObject *current = object;
662
+ while (current) {
663
+ const QString className = current->metaObject ()->className ();
664
+ const QString objectName = current->objectName ();
665
+ const QString name = className == objectName
666
+ ? className : QStringLiteral (" %1:%2" ).arg (objectName, className);
667
+ // Skip some default widgets.
668
+ if ( name != QLatin1String (" :QWidget" )
669
+ && !name.startsWith (QLatin1String (" qt_scrollarea_viewport:QWidget" )) )
670
+ {
671
+ if (!result.isEmpty ())
672
+ result.append (' <' );
673
+ result.append (name);
674
+ const QString text = current->property (" text" ).toString ()
675
+ .remove (' &' )
676
+ // Remove HTML tags
677
+ .remove (QRegularExpression (QStringLiteral (" </?[^>]*>" )));
678
+ if ( !text.isEmpty () )
679
+ result.append (QStringLiteral (" '%1'" ).arg (text));
680
+ }
681
+ current = current->parent ();
682
+ }
683
+ return result;
684
+ }
685
+
686
+ bool matchesProperties (QObject *object, const QStringList &properties)
687
+ {
688
+ for (auto it = properties.cbegin (); it != properties.cend (); ++it) {
689
+ const QString key = it->section (' =' , 0 , 0 );
690
+ const QString value = it->section (' =' , 1 , 1 );
691
+ if ( value.isEmpty () ) {
692
+ if ( object->objectName () != key && object->metaObject ()->className () != key )
693
+ return false ;
694
+ }
695
+
696
+ const QVariant propValue = object->property (key.toUtf8 ());
697
+ if ( propValue.toString () != value )
698
+ return false ;
699
+ }
700
+
701
+ return true ;
702
+ }
703
+
704
+ QWidget *findWidgetWithProperties (const QString &definition, QWidget *parent)
705
+ {
706
+ QStringList names = definition.split (' <' );
707
+ return std::accumulate (
708
+ names.cbegin (), names.cend (), parent,
709
+ [](QWidget *parent, const QString &name) -> QWidget* {
710
+ if (parent == nullptr )
711
+ return nullptr ;
712
+
713
+ const QStringList props = name.split (' |' , Qt::SkipEmptyParts);
714
+ for (QWidget *child : parent->findChildren <QWidget*>()) {
715
+ if (child->isVisible () && matchesProperties (child, props)) {
716
+ COPYQ_LOG ( QStringLiteral (" Found target: %1" ).arg (objectAddress (child)) );
717
+ return child;
718
+ }
719
+ }
720
+ log ( QStringLiteral (" Failed to find mouse action target: %1" ).arg (name), LogError );
721
+ return nullptr ;
722
+ });
723
+ }
724
+ #endif
725
+
654
726
} // namespace
655
727
656
728
#ifdef HAS_TESTS
@@ -682,11 +754,11 @@ class KeyClicker final : public QObject {
682
754
log ( QString (" Failed to send key press to target widget" )
683
755
+ QLatin1String (qApp->applicationState () == Qt::ApplicationActive ? " " : " \n App is INACTIVE!" )
684
756
+ " \n Expected: /" + expectedWidgetName.pattern () + " /"
685
- + " \n Actual: " + keyClicksTargetDescription (actual)
686
- + " \n Popup: " + keyClicksTargetDescription (popup)
687
- + " \n Widget: " + keyClicksTargetDescription (widget)
688
- + " \n Window: " + keyClicksTargetDescription (window)
689
- + " \n Modal: " + keyClicksTargetDescription (modal)
757
+ + " \n Actual: " + objectAddress (actual)
758
+ + " \n Popup: " + objectAddress (popup)
759
+ + " \n Widget: " + objectAddress (widget)
760
+ + " \n Window: " + objectAddress (window)
761
+ + " \n Modal: " + objectAddress (modal)
690
762
+ " \n Title: " + currentWindowTitle
691
763
, LogError );
692
764
@@ -711,7 +783,7 @@ class KeyClicker final : public QObject {
711
783
return ;
712
784
}
713
785
714
- auto widgetName = keyClicksTargetDescription (widget);
786
+ const QString widgetName = objectAddress (widget);
715
787
if ( !expectedWidgetName.pattern ().isEmpty ()
716
788
&& !expectedWidgetName.match (widgetName).hasMatch () )
717
789
{
@@ -730,7 +802,7 @@ class KeyClicker final : public QObject {
730
802
if ( qobject_cast<QCheckBox*>(widget) )
731
803
waitFor (100 );
732
804
733
- COPYQ_LOG ( QString (" Sending keys \" %1\" to %2." )
805
+ COPYQ_LOG ( QString (" Sending event \" %1\" to %2." )
734
806
.arg (keys, widgetName) );
735
807
736
808
const auto popupMessage = QString::fromLatin1 (" %1 (%2)" )
@@ -740,13 +812,55 @@ class KeyClicker final : public QObject {
740
812
notification->setIcon (IconKeyboard);
741
813
notification->setInterval (2000 );
742
814
743
- if ( keys.startsWith (" :" ) ) {
744
- const auto text = keys.mid (1 );
815
+ static const auto keyClicksPrefix = QLatin1String (" :" );
816
+ static const auto mousePrefix = QLatin1String (" mouse|" );
817
+ if ( keys.startsWith (keyClicksPrefix) ) {
818
+ const auto text = keys.mid (keyClicksPrefix.size ());
745
819
746
820
QTest::keyClicks (widget, text, Qt::NoModifier, 0 );
747
821
748
822
// Increment key clicks sequence number after typing all the text.
749
823
m_succeeded = true ;
824
+ } else if ( keys.startsWith (mousePrefix) ) {
825
+ const QString action = keys.section (' |' , 1 , 1 );
826
+ const QString properties = keys.section (' |' , 2 );
827
+ static const auto mousePress = QStringLiteral (" PRESS" );
828
+ static const auto mouseRelease = QStringLiteral (" RELEASE" );
829
+ static const auto mouseClick = QStringLiteral (" CLICK" );
830
+ static const auto mouseMove = QStringLiteral (" MOVE" );
831
+ static const QStringList validActions = {
832
+ mousePress, mouseRelease, mouseClick, mouseMove};
833
+ if ( properties.isEmpty () || !validActions.contains (action) ) {
834
+ log ( QStringLiteral (" Failed to match mouse action: %1" ).arg (keys), LogError );
835
+ log (" Mouse action format must be: " " mouse|{PRESS|RELEASE}|OBJECT_PATH" );
836
+ m_failed = true ;
837
+ return ;
838
+ }
839
+ QPointer<QWidget> source = findWidgetWithProperties (properties, m_wnd);
840
+ if (!source) {
841
+ m_failed = true ;
842
+ return ;
843
+ }
844
+ // Don't stop when modal window is open.
845
+ runAfterInterval (delay, [=](){
846
+ if (!source) {
847
+ log (" Target widget was destroyed" , LogError);
848
+ return ;
849
+ }
850
+ if (!source->isVisible ()) {
851
+ log (" Target widget is no longer visible" , LogError);
852
+ return ;
853
+ }
854
+ if (action == mousePress)
855
+ QTest::mousePress (source, Qt::LeftButton);
856
+ else if (action == mouseRelease)
857
+ QTest::mouseRelease (source, Qt::LeftButton);
858
+ else if (action == mouseClick)
859
+ QTest::mouseClick (source, Qt::LeftButton);
860
+ else if (action == mouseMove)
861
+ QTest::mouseMove (source);
862
+ });
863
+ m_succeeded = true ;
750
864
} else {
751
865
const QKeySequence shortcut (keys, QKeySequence::PortableText);
752
866
@@ -766,7 +880,7 @@ class KeyClicker final : public QObject {
766
880
0 );
767
881
}
768
882
769
- COPYQ_LOG ( QString (" Key \" %1\" sent to %2." )
883
+ COPYQ_LOG ( QString (" Event \" %1\" sent to %2." )
770
884
.arg (keys, widgetName) );
771
885
}
772
886
@@ -776,49 +890,21 @@ class KeyClicker final : public QObject {
776
890
m_failed = false ;
777
891
778
892
// Don't stop when modal window is open.
779
- auto t = new QTimer (m_wnd);
780
- t->setSingleShot (true );
781
- QObject::connect ( t, &QTimer::timeout, this , [=]() {
782
- keyClicks (expectedWidgetName, keys, delay, retry);
783
- t->deleteLater ();
784
- });
785
- t->start (delay);
893
+ runAfterInterval (delay, [=](){ keyClicks (expectedWidgetName, keys, delay, retry); });
786
894
}
787
895
788
896
bool succeeded () const { return m_succeeded; }
789
897
bool failed () const { return m_failed; }
790
898
791
899
private:
792
- static QString keyClicksTargetDescription (QWidget *widget)
900
+ template <typename Callable>
901
+ void runAfterInterval (int delay, Callable func)
793
902
{
794
- if (widget == nullptr )
795
- return " None" ;
796
-
797
- const auto className = widget->metaObject ()->className ();
798
-
799
- auto widgetName = QString::fromLatin1 (" %1:%2" )
800
- .arg (widget->objectName (), className);
801
-
802
- const auto window = widget->window ();
803
- if (window && widget != window) {
804
- widgetName.append (
805
- QString::fromLatin1 (" in %1:%2" )
806
- .arg (window->objectName (), window->metaObject ()->className ())
807
- );
808
- }
809
-
810
- auto parent = widget->parentWidget ();
811
- while (parent) {
812
- if ( parent != window && !parent->objectName ().startsWith (" qt_" ) ) {
813
- widgetName.append (
814
- QString::fromLatin1 (" in %1:%2" )
815
- .arg (parent->objectName (), parent->metaObject ()->className ())
816
- );
817
- }
818
- parent = parent->parentWidget ();
819
- }
820
-
821
- return widgetName;
903
+ auto t = new QTimer (m_wnd);
904
+ t->setSingleShot (true );
905
+ QObject::connect (t, &QTimer::timeout, m_wnd, func);
906
+ QObject::connect (t, &QTimer::timeout, t, &QTimer::deleteLater);
907
+ t->start (delay);
822
908
}
823
909
824
910
QWidget *keyClicksTarget ()
@@ -2015,8 +2101,11 @@ void ScriptableProxy::sendKeys(const QString &expectedWidgetName, const QString
2015
2101
{
2016
2102
INVOKE2 (sendKeys, (expectedWidgetName, keys, delay));
2017
2103
Q_ASSERT ( keyClicker ()->succeeded () || keyClicker ()->failed () );
2018
- keyClicker ()->sendKeyClicks (
2019
- QRegularExpression (expectedWidgetName), keys, delay, 10 );
2104
+ const QRegularExpression re = QRegularExpression (
2105
+ QString (expectedWidgetName)
2106
+ .replace (QLatin1String (" <" ), QLatin1String (" .*<.*" ))
2107
+ );
2108
+ keyClicker ()->sendKeyClicks (re, keys, delay, 10 );
2020
2109
}
2021
2110
2022
2111
bool ScriptableProxy::sendKeysSucceeded ()
0 commit comments