// krazy:excludeall=qclasses ////////////////////////////////////////////////////////////////////////////// // oxygensimulator.cpp // simulates event chain passed to the application // ------------------- // // Copyright (c) 2010 Hugo Pereira Da Costa // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. ////////////////////////////////////////////////////////////////////////////// #include "oxygensimulator.h" #include "moc_oxygensimulator.cpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Oxygen { //_______________________________________________________________________ bool Simulator::_grabMouse = true; int Simulator::_defaultDelay = 250; //_______________________________________________________________________ Simulator::~Simulator( void ) {} //_______________________________________________________________________ void Simulator::wait( int delay ) { _events.push_back( Event( Event::Wait, 0, delay ) ); } //_______________________________________________________________________ void Simulator::click( QWidget* receiver, int delay ) { QPoint position; if( QCheckBox* checkbox = qobject_cast( receiver ) ) { QStyleOptionButton option; option.initFrom( checkbox ); position = checkbox->style()->subElementRect( QStyle::SE_CheckBoxIndicator, &option, checkbox).center(); } else if( QRadioButton* radiobutton = qobject_cast( receiver ) ) { QStyleOptionButton option; option.initFrom( radiobutton ); position = radiobutton->style()->subElementRect( QStyle::SE_RadioButtonIndicator, &option, radiobutton).center(); } else { position = receiver->rect().center(); } click( receiver, position, delay ); } //_______________________________________________________________________ void Simulator::click( QWidget* receiver, const QPoint& position, int delay ) { Event event( Event::Click, receiver, delay ); event._position = position; _events.push_back( event ); } //_______________________________________________________________________ void Simulator::slide( QWidget* receiver, const QPoint& position, int delay ) { Event event( Event::Slide, receiver, delay ); event._position = position; _events.push_back( event ); } //_______________________________________________________________________ void Simulator::selectItem( QWidget* receiver, int row, int column, int delay ) { Event event( Event::SelectItem, receiver, delay ); event._position = QPoint( column, row ); _events.push_back( event ); } //_______________________________________________________________________ void Simulator::selectComboBoxItem( QWidget* receiver, int index, int delay ) { Event event( Event::SelectComboBoxItem, receiver, delay ); event._position.setX( index ); _events.push_back( event ); } //_______________________________________________________________________ void Simulator::selectMenuItem( QWidget* receiver, int index, int delay ) { Event event( Event::SelectMenuItem, receiver, delay ); event._position.setX( index ); _events.push_back( event ); } //_______________________________________________________________________ void Simulator::selectTab( QTabWidget* tabwidget, int index, int delay ) { foreach( QObject* child, tabwidget->children() ) { if( QTabBar* tabbar = qobject_cast( child ) ) { selectTab( tabbar, index, delay ); break; } } } //_______________________________________________________________________ void Simulator::selectTab( QTabBar* receiver, int index, int delay ) { Event event( Event::SelectTab, receiver, delay ); event._position.setX( index ); _events.push_back( event ); } //_______________________________________________________________________ void Simulator::writeText( QWidget* receiver, QString text, int delay ) { Event event( Event::WriteText, receiver, delay ); event._text = text; _events.push_back( event ); } //_______________________________________________________________________ void Simulator::clearText( QWidget* receiver, int delay ) { _events.push_back( Event( Event::ClearText, receiver, delay ) ); } //_______________________________________________________________________ void Simulator::run( void ) { if( _events.isEmpty() ) return; // clear abort state _aborted = false; emit stateChanged( true ); foreach( const Event& event, _events ) { if( _aborted ) { _events.clear(); return; } processEvent( event ); } // add last event to reset previousWidget and previousPosition if( _previousWidget ) { postEvent( _previousWidget.data(), QEvent::Leave ); if( _previousWidget.data()->testAttribute( Qt::WA_Hover ) ) { const QPoint oldPosition( _previousWidget.data()->mapFromGlobal( _previousPosition ) ); const QPoint newPosition( _previousWidget.data()->mapFromGlobal( QPoint( -1, -1 ) ) ); postHoverEvent( _previousWidget.data(), QEvent::HoverLeave, newPosition, oldPosition ); } _previousWidget.clear(); _previousPosition = QPoint(-1, -1 ); } _events.clear(); emit stateChanged( false ); return; } //_______________________________________________________________________ void Simulator::abort( void ) { _aborted = true; emit stateChanged( true ); } //_______________________________________________________________________ void Simulator::timerEvent( QTimerEvent* event ) { if( event->timerId() == _timer.timerId() ) { _timer.stop(); } else if( event->timerId() == _pendingEventsTimer.timerId() ) { if( _aborted ) { foreach( QEvent* event, _pendingEvents ) { delete event; } _pendingEvents.clear(); _pendingWidget.clear(); } else if( _pendingWidget && _pendingWidget.data()->isVisible() ) { _pendingEventsTimer.stop(); foreach( QEvent* event, _pendingEvents ) { if( event->type() == QEvent::MouseMove ) { QPoint position( static_cast( event )->pos() ); moveCursor( _pendingWidget.data()->mapToGlobal( position ) ); } postQEvent( _pendingWidget.data(), event ); postDelay( 150 ); } _pendingEvents.clear(); _pendingWidget.clear(); } } else return QObject::timerEvent( event ); } //_______________________________________________________________________ void Simulator::processEvent( const Event& event ) { if( _aborted ) return; if( !event._receiver ) { if( event._type == Event::Wait ) { postDelay( event._delay ); } return; } QWidget* receiver( event._receiver.data() ); switch( event._type ) { // click event case Event::Click: { // enter widget or move cursor to relevant position if( !enter( receiver, event._position, event._delay ) ) { moveCursor( receiver->mapToGlobal( event._position ) ); postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, event._position ); } postMouseClickEvent( receiver, Qt::LeftButton, event._position ); break; } // slide case Event::Slide: { const QPoint& delta( event._position ); // calculate begin position depending on widget type QPoint begin; if( const QSlider* slider = qobject_cast( receiver ) ) { // this is copied from QSlider::initStyleOption QStyleOptionSlider option; option.initFrom( slider ); option.orientation = slider->orientation(); option.sliderPosition = slider->sliderPosition(); option.minimum = slider->minimum(); option.maximum = slider->maximum(); option.upsideDown = (slider->orientation() == Qt::Horizontal) ? ( slider->invertedAppearance() != (option.direction == Qt::RightToLeft)) : (!slider->invertedAppearance() ); QRect handleRect( slider->style()->subControlRect( QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, slider ) ); if( !handleRect.isValid() ) break; begin = handleRect.center(); } else if( const QScrollBar* scrollbar = qobject_cast( receiver ) ) { // this is copied from QSlider::initStyleOption QStyleOptionSlider option; option.initFrom( scrollbar ); option.orientation = scrollbar->orientation(); option.sliderPosition = scrollbar->sliderPosition(); option.minimum = scrollbar->minimum(); option.maximum = scrollbar->maximum(); option.upsideDown = scrollbar->invertedAppearance(); if( scrollbar->orientation() == Qt::Horizontal ) { option.state |= QStyle::State_Horizontal; } QRect handleRect( scrollbar->style()->subControlRect( QStyle::CC_ScrollBar, &option, QStyle::SC_ScrollBarSlider, scrollbar ) ); if( !handleRect.isValid() ) break; begin = handleRect.center(); } else { begin = receiver->rect().center(); } // enter widget or move cursor to relevant position if( !enter( receiver, begin, event._delay ) ) { moveCursor( receiver->mapToGlobal( begin ) ); postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, begin ); } const QPoint end( begin + delta ); postMouseEvent( receiver, QEvent::MouseButtonPress, Qt::LeftButton, begin, Qt::LeftButton ); setFocus( receiver ); postDelay( 50 ); const int steps = 10; for( int i=0; imapToGlobal( current ), 1 ); postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, current, Qt::LeftButton, Qt::NoModifier ); postDelay( 20 ); } postMouseEvent( receiver, QEvent::MouseButtonRelease, Qt::LeftButton, end ); break; } case Event::SelectItem: { const QAbstractItemView* view = qobject_cast( receiver ); if( !( view && view->model() ) ) break; const int column( event._position.x() ); const int row( event._position.y() ); // find index const QModelIndex modelIndex( view->model()->index( row, column ) ); if( !modelIndex.isValid() ) break; // get rect QRect r( view->visualRect( modelIndex ) ); if( !r.isValid() ) break; // enter widget or move cursor to relevant position const QPoint position( r.center() ); if( !enter( view->viewport(), position, event._delay ) ) { moveCursor( view->viewport()->mapToGlobal( position ) ); postMouseEvent( view->viewport(), QEvent::MouseMove, Qt::NoButton, position ); postDelay( event._delay ); } postMouseClickEvent( view->viewport(), Qt::LeftButton, position ); break; } case Event::SelectComboBoxItem: { QComboBox* combobox = qobject_cast( receiver ); if( !combobox ) break; // get arrow rect QStyleOptionComboBox option; option.initFrom( combobox ); QRect arrowRect( combobox->style()->subControlRect( QStyle::CC_ComboBox, &option, QStyle::SC_ComboBoxArrow, combobox ) ); // enter widget or move cursor to relevant position QPoint position( arrowRect.center() ); if( !enter( combobox, position, event._delay ) ) { moveCursor( combobox->mapToGlobal( position ) ); postMouseEvent( combobox, QEvent::MouseMove, Qt::NoButton, position ); postDelay( event._delay ); } postMouseClickEvent( combobox, Qt::LeftButton, position ); // select item in view QAbstractItemView* view = combobox->view(); const int row( event._position.x() ); const int column( 0 ); // find index const QModelIndex modelIndex( view->model()->index( row, column ) ); if( !modelIndex.isValid() ) break; // get rect QRect r( view->visualRect( modelIndex ) ); if( !r.isValid() ) break; // send event position = QPoint( r.center() ); moveCursor( view->viewport()->mapToGlobal( position ) ); postMouseEvent( view->viewport(), QEvent::MouseMove, Qt::NoButton, position, Qt::NoButton, Qt::NoModifier ); postDelay(100); postMouseClickEvent( view->viewport(), Qt::LeftButton, position ); break; } case Event::SelectMenuItem: { // retrieve menu QMenu* menu( 0 ); if( const QToolButton* button = qobject_cast( receiver ) ) menu = button->menu(); else if( const QPushButton* button = qobject_cast( receiver ) ) menu = button->menu(); // abort if not found if( !menu ) break; // get action and geometry const int row( event._position.x() ); QList actions( menu->actions() ); if( row >= actions.size() ) break; menu->sizeHint(); QRect r( menu->actionGeometry( actions[row] ) ); if( !r.isValid() ) break; /*! HACK: As soon as leftMouseButton is pressed on a button with menu, the menu is shown and code is interrupted until an action is selected in the menu. As a consequence, one must first generate the events, execute them with a delay, and then click on the button (before delay is expired). This way, the menu events will be executed even if the menu is visible (and blocking further code execution). */ QPoint position( r.center() ); _pendingWidget = menu; _pendingEvents.push_back( new QMouseEvent( QEvent::MouseMove, position, Qt::NoButton, Qt::NoButton, Qt::NoModifier ) ); _pendingEvents.push_back( new QMouseEvent( QEvent::MouseButtonPress, position, Qt::LeftButton, Qt::NoButton, Qt::NoModifier ) ); _pendingEvents.push_back( new QMouseEvent( QEvent::MouseButtonRelease, position, Qt::LeftButton, Qt::NoButton, Qt::NoModifier ) ); _pendingEventsTimer.start( 10, this ); // enter widget or move cursor to relevant position position = receiver->rect().center(); if( !enter( receiver, position, event._delay ) ) { moveCursor( receiver->mapToGlobal( position ) ); postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, position ); postDelay( event._delay ); } // click postMouseEvent( receiver, QEvent::MouseButtonPress, Qt::LeftButton, position, Qt::NoButton, Qt::NoModifier ); break; } case Event::SelectTab: { const QTabBar* tabbar = qobject_cast( receiver ); if( !tabbar ) break; const int index( event._position.x() ); const QRect r( tabbar->tabRect( index ) ); if( !r.isValid() ) break; // enter widget or move cursor to relevant position const QPoint position( r.center() ); if( !enter( receiver, position, event._delay ) ) { moveCursor( receiver->mapToGlobal( position ) ); postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, position ); postDelay( event._delay ); } postMouseClickEvent( receiver, Qt::LeftButton, position ); break; } case Event::WriteText: { enter( receiver, receiver->rect().center(), event._delay ); setFocus( receiver ); const QString& text( event._text ); for( int i=0; i < text.length(); ++i ) { const Qt::Key key( toKey( text.at(i) ) ); const QString local( text.at(i) ); postKeyEvent( receiver, QEvent::KeyPress, key, local, Qt::NoModifier ); postKeyEvent( receiver, QEvent::KeyRelease, key, local, Qt::NoModifier ); postDelay( 20 ); } break; } case Event::ClearText: { enter( receiver, receiver->rect().center(), event._delay ); setFocus( receiver ); postKeyEvent( receiver, QEvent::KeyPress, Qt::Key_A, "a", Qt::ControlModifier ); postKeyEvent( receiver, QEvent::KeyRelease, Qt::Key_A, "a", Qt::ControlModifier ); postDelay( 20 ); postKeyClickEvent( receiver, Qt::Key_Backspace, QString() ); } default: break; } // delay postDelay( event._delay ); return; } //_______________________________________________________________________ void Simulator::postEvent( QWidget* receiver, QEvent::Type type ) const { postQEvent( receiver, new QEvent( type ) ); } //_______________________________________________________________________ void Simulator::postHoverEvent( QWidget* receiver, QEvent::Type type, const QPoint& newPosition, const QPoint& oldPosition ) const { postQEvent( receiver, new QHoverEvent( type, newPosition, oldPosition ) ); } //_______________________________________________________________________ bool Simulator::enter( QWidget* receiver, const QPoint& position, int delay ) { if( receiver == _previousWidget.data() ) return false; // store position moveCursor( receiver->mapToGlobal( position ) ); // leave previous widget if( _previousWidget ) { postEvent( _previousWidget.data(), QEvent::Leave ); if( _previousWidget.data()->testAttribute( Qt::WA_Hover ) ) { const QPoint oldPosition( _previousWidget.data()->mapFromGlobal( _previousPosition ) ); const QPoint newPosition( _previousWidget.data()->mapFromGlobal( receiver->mapToGlobal( position ) ) ); postHoverEvent( _previousWidget.data(), QEvent::HoverLeave, newPosition, oldPosition ); } } // enter or move in current widget if( !receiver->rect().contains( receiver->mapFromGlobal( _previousPosition ) ) ) { // enter current widget if needed postEvent( receiver, QEvent::Enter ); if( receiver->testAttribute( Qt::WA_Hover ) ) { const QPoint oldPosition( receiver->mapFromGlobal( _previousPosition ) ); const QPoint newPosition( position ); postHoverEvent( receiver, QEvent::HoverEnter, newPosition, oldPosition ); } } else if( receiver->mapFromGlobal( _previousPosition ) != position ) { // move mouse if needed postMouseEvent( receiver, QEvent::MouseMove, Qt::NoButton, position ); if( receiver->testAttribute( Qt::WA_Hover ) ) { const QPoint oldPosition( receiver->mapFromGlobal( _previousPosition ) ); const QPoint newPosition( position ); postHoverEvent( receiver, QEvent::HoverMove, newPosition, oldPosition ); } } // update previous widget and position _previousWidget = receiver; _previousPosition = receiver->mapToGlobal( position ); postDelay( delay ); return true; } //_______________________________________________________________________ void Simulator::postMouseClickEvent( QWidget* receiver, Qt::MouseButton button, const QPoint& position ) { // button press and button release postMouseEvent( receiver, QEvent::MouseButtonPress, button, position, button ); setFocus( receiver ); postDelay(50); postMouseEvent( receiver, QEvent::MouseButtonRelease, button, position, button ); } //_______________________________________________________________________ void Simulator::postMouseEvent( QWidget* receiver, QEvent::Type type, Qt::MouseButton button, const QPoint& position, Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers ) const { postQEvent( receiver, new QMouseEvent( type, position, receiver->mapToGlobal( position ), button, buttons, modifiers ) ); } //_______________________________________________________________________ void Simulator::postKeyClickEvent( QWidget* receiver, Qt::Key key, QString text, Qt::KeyboardModifiers modifiers ) const { postKeyModifiersEvent( receiver, QEvent::KeyPress, modifiers ); postKeyEvent( receiver, QEvent::KeyPress, key, text, modifiers ); postKeyEvent( receiver, QEvent::KeyRelease, key, text, modifiers ); postKeyModifiersEvent( receiver, QEvent::KeyRelease, modifiers ); } //_______________________________________________________________________ void Simulator::postKeyModifiersEvent( QWidget* receiver, QEvent::Type type, Qt::KeyboardModifiers modifiers ) const { if( modifiers == Qt::NoModifier ) return; switch( type ) { case QEvent::KeyPress: { if( modifiers & Qt::ShiftModifier) { postKeyEvent( receiver, QEvent::KeyPress, Qt::Key_Shift, QString() ); } if( modifiers & Qt::ControlModifier ) { postKeyEvent( receiver, QEvent::KeyPress, Qt::Key_Control, QString(), modifiers & Qt::ShiftModifier ); } if( modifiers & Qt::AltModifier ) { postKeyEvent( receiver, QEvent::KeyPress, Qt::Key_Alt, QString(), modifiers & (Qt::ShiftModifier|Qt::ControlModifier) ); } if( modifiers & Qt::MetaModifier ) { postKeyEvent( receiver, QEvent::KeyPress, Qt::Key_Meta, QString(), modifiers & (Qt::ShiftModifier|Qt::ControlModifier|Qt::AltModifier) ); } break; } case QEvent::KeyRelease: { if( modifiers & Qt::MetaModifier ) { postKeyEvent( receiver, QEvent::KeyRelease, Qt::Key_Meta, QString() ); } if( modifiers & Qt::AltModifier ) { postKeyEvent( receiver, QEvent::KeyRelease, Qt::Key_Alt, QString(), modifiers & Qt::MetaModifier ); } if( modifiers & Qt::ControlModifier ) { postKeyEvent( receiver, QEvent::KeyRelease, Qt::Key_Control, QString(), modifiers & (Qt::MetaModifier|Qt::AltModifier) ); } if( modifiers & Qt::ShiftModifier) { postKeyEvent( receiver, QEvent::KeyRelease, Qt::Key_Shift, QString(), modifiers & (Qt::MetaModifier|Qt::AltModifier|Qt::ControlModifier) ); } } default: break; } } //_______________________________________________________________________ void Simulator::postKeyEvent( QWidget* receiver, QEvent::Type type, Qt::Key key, QString text, Qt::KeyboardModifiers modifiers ) const { postQEvent( receiver, new QKeyEvent( type, key, modifiers, text ) ); } //_______________________________________________________________________ void Simulator::postDelay( int delay ) { // check value if( delay == -1 ) delay = _defaultDelay; if( delay <= 0 ) return; // this is largely inspired from qtestlib's qsleep implementation _timer.start( delay, this ); while( _timer.isActive() ) { // flush events in loop QCoreApplication::processEvents(QEventLoop::AllEvents, delay); int ms( 10 ); // sleep struct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 }; nanosleep(&ts, NULL); } } //_______________________________________________________________________ void Simulator::moveCursor( const QPoint& position, int steps ) { // do nothing if mouse grab is disabled if( !_grabMouse ) return; if( _aborted ) return; const QPoint begin( QCursor::pos() ); const QPoint end( position ); if( begin == end ) return; if( steps > 1 ) { for( int i = 0; ifocusPolicy() != Qt::NoFocus ) { receiver->setFocus(); } } //_______________________________________________________________________ Qt::Key Simulator::toKey( QChar a ) const { return (Qt::Key) QKeySequence( a )[0]; } //_______________________________________________________________________ void Simulator::postQEvent( QWidget* receiver, QEvent* event ) const { if( _aborted ) delete event; else qApp->postEvent( receiver, event ); } }