Qt 5 Disabling Click-and-Hold - c++

I'm trying to solve an issue in a Qt5 application that's caused by a touchscreen driver I wrote a couple years ago. The problem being that my touchscreen driver generates mouse press events to simulate touch behavior. This was because at the time I wrote it, my team was using an outdated Linux OS that didn't have a native touch driver.
The problem is that when I touch the +/- buttons (the scroll buttons that increase/decrease the slider by a single tick, or cause it to rapidly scroll if held), the slider rapidly slides up/down as if I had clicked and held the button. This is because my driver uses QApplication::postEvent() to dispatch the mouse event from a separate thread, and the delay in between touch and release is just long enough for it to register a click-and-hold type event, though the release event doesn't stop the sliding.
I would rewrite the application to use the now available native touch driver, but it doesn't offer enough control to be able to do what my driver does (my driver can generate left, right, or middle mouse events, whereas the native driver only provides left).
I tried rewriting the driver to use QApplication::sendEvent() instead, but that was causing a segmentation fault that I couldn't figure out the cause of.
So is there a way I can disable the "click-and-hold" type behavior on the QSlider itself? So that I can still tap the buttons, but they'll only increase/decrease by a single tick at a time?
EDIT: Here's an example of how I'm generating the "touch" events via my driver.
void InputHandler::touchPress(TouchPoint * tp, QWidget * widget) {
QPoint screenPos(tp->cx(), tp->cy());
QWidget * target = widget;
if(target == NULL) target = QApplication::widgetAt(screenPos);
if(target != NULL) {
QPoint local = target->mapFromGlobal(screenPos);
QMouseEvent * press = new QMouseEvent(QEvent::MouseButtonPress, local, screenPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
QApplication::postEvent(target, press);
DBG(" -- touch press on: " << typeid(*widget).name());
// Check to see if the target is a QLineEdit or QComboBox, and if so, set focus on it.
QLineEdit * lineEdit = dynamic_cast<QLineEdit*>(target);
QComboBox * comboBox = dynamic_cast<QComboBox*>(target);
if(lineEdit) lineEdit->setFocus(Qt::MouseFocusReason);
if(comboBox) comboBox->setFocus(Qt::MouseFocusReason);
}
}
EDIT 2: So a coworker played around with the slider and pointed out that he pressed the +/- buttons, causing it to slide, and then he clicks the "reset" button we have to reset all controls on the form, the slider would reset and then continue sliding, indicating that the touch press was never released. He then would click the +/- buttons normally with the mouse and reset again and it would stop. So even though the logs indicate that the mousePressEvent and mouseReleaseEvent methods are being triggered by my events, they don't seem to have the same behavior as using the mouse.
Any ideas?
EDIT 3: If it helps, I added an eventFilter and printed out every event type that the QSlider receives, from when I first touch the screen to when I release. The events received are:
Mouse Press (2)
Tooltip Change (184)
Paint (12)
Mouse Move (5)
Mouse Move (5)
Mouse Release (3)
But then after the release event, I get spammed in the logs with QEvent::Timer events, which does not happen when I simply click with the mouse, as opposed to touching the slider with the touchscreen. If I then tap somewhere else, drag the slider, or click the slider with the actual mouse, these timer events stop.
Why is my generated QMouseEvent causing these QEvent::Timer events to be generated when I tap the slider with the touchscreen, but not with the mouse?

A minimal example to generate mouse press/release events that are correctly handled by QSlider is:
#include <QApplication>
#include <QMainWindow>
#include <QMouseEvent>
#include <QSlider>
#include <QTimer>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QMainWindow m;
QSlider *slider = new QSlider();
m.setCentralWidget(slider);
m.resize(100, 100);
m.show();
QTimer::singleShot(1000, [&](){
a.postEvent(slider, new QMouseEvent(QEvent::MouseButtonPress, QPoint(50,50), m.mapToGlobal(QPoint(50,50)), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier));
a.postEvent(slider, new QMouseEvent(QEvent::MouseButtonRelease, QPoint(50,50), m.mapToGlobal(QPoint(50,50)), Qt::LeftButton, Qt::NoButton, Qt::NoModifier));
});
return a.exec();
}
This example decreases the slider value with 1 tick after 1 second.
Note that it is important to pass Qt::NoButton as buttons parameters to the QEvent::MouseButtonRelease event, otherwise the slider will continue moving to the current mouse position (see QAbstractSlider::setRepeatAction), as QSlider thinks the button is still pressed. I assume this behaviour is what you call 'press-and-hold'.

Related

Qt transfer mouse move events to new window

I'm handling mouse events (through an event filter) on a QTabBar to detect when the user clicks and drags a tab to tear it off. When this happens, I remove the current widget from the QTabWidget and create a new top level widget that I add it to so that it's detached and floating, just like when you tear off a tab in Chrome. This new floating window is a custom frameless widget I made that has a custom titlebar that I handle mouse events on to allow the user to drag the window around the desktop.
The problem I'm having is that when you click and drag the tab to pull it off and the new top level window is created, I can't seem to get the application to continue dragging the new window without the user clicking and dragging on my titlebar. I'd like for the original drag motion to just transfer to the new widget so that the use can keep dragging it until he releases the mouse button.
I've tried creating a "fake" QMouseEvent to pass to my title bar (by calling QCoreApplication::sendEvent(object, event) to make it think it's been clicked on, but it doesn't receive any mouse move events unless you actually click on it. I'm open to other ideas.
Update: I added some debugging statements and it looks like once I detach the tab and create the new floating window, the QMainWindow continues to receive the mouse move events until I release the mouse button. I'll try adding some code to forward these mouse events on to the new floating window, but that feels kinda hacky.
Correction: The QMainWindow is not receiving the mouse move events, an object named "MainWindowWindow" is, which is a QWidgetWindow that I guess is a private type used to manage top level windows?
Ok, I got it working, but I don't like it. As I said in my correction, once the tab is detached, the QWidgetWindow starts receiving the mouse move events. This is a private type that's not exposed via the API.
What I ended up doing was installing an event filter on the application and looking for mouse events on an object that inherits from QWidgetWindow. I then directly call new drag() and endDrag() methods that I added to my floating window class. In my eventFilter method, it looks like this.
if (watched->inherits("QWidgetWindow"))
{
if (floater) // <- the floating window that was recently detached
{
if (event->type() == QEvent::MouseMove)
{
floater->drag(QCursor::pos()); // <-- Pass global cursor pos
}
else if (event->type() == QEvent::MouseRelease)
{
floater->endDrag();
floater = nullptr;
}
}
}
Like I said, if feels dirty because I A) am checking for events on a private type that I'm not really supposed to know about, and B) telling another window to drag around when it has code to do this itself. But it works, and I've spent enough time working on this. If someone has a more elegant solution, I'm happy to hear it.

Qt: how to freeze some buttons for a specific amount of time?

I'm trying to build a simple memory game with Qt 5.11.1 and C++, where you get a few tiles on screen and you have to click on two and try to match the images they show.
My tiles are implemented as QPushButtons. Each time you click on one an image is displayed (by calling a showImage()method that changes the button background). When a second tile is clicked, if there is a match the two buttons are disabled so you can't click on them again (and you get a higher score). However, if you didn't get a match, the two tiles you just clicked will go back to their initial state (showing no image) after 1 second (this allows the user to "memorize" which image was showing up on each tile).
Whenever you click on a "tile" (button) it becomes disabled (button->setEnabled(false)). If after clicking a second tile there was no match, then both tiles are turned back and then setEnabled(true) again. I'm using a single shot QTimer to call the method that will turn back the tiles:
QTimer::singleShot(1000, this, SLOT(turnTilesBack()));
firstTile->setEnabled(true);
secondTile->setEnabled(true);
Everything is working as expected, except for one thing: as QTimer runs in its own thread (or so I understand from what I read) all of the available tiles remain enabled during the 1000 milisecond lapse, allowing the user to continue clicking on them. However, when there is no match, I'd like to "freeze" the buttons until the QTimer has timed out so the user can't continue playing until the tiles have turned back.
So instead of using the QTimer I've trying this solution which I saw on this question (How do I create a pause/wait function using Qt?):
QTime dieTime= QTime::currentTime().addSecs(1);
while (QTime::currentTime() < dieTime)
turnTilesBack();
although I removed this line: QCoreApplication::processEvents(QEventLoop::AllEvents, 100); as this would cause the main thread not to freeze and buttons would still be clickable.
But with this approach, whenever the user clicks on the second tile, if there is no match the image is not even displayed, even when my showImage() method is called before the code above, and I'm not sure why this is. So the user knows there was no match because after 1 second the tiles go back to their initial state, but they never got to see the image on the second button.
As another approach, I also though of disabling all buttons and then after the single shot QTimer times out, re-enabling back only the ones that have not been matched yet. But this would require additional logic to keep track of which tiles have been matched. So for now I'm sticking to the
Is there a cleaner solution? Maybe there's a way to make the QTimer freeze the main thread until it times out?
An easy way to enable/disable the entire group of QPushButtons is to place them on an intermediate widget (in the below example I've used a QFrame)
If you want to disable all the QPushButtons, you just disable the frame, and all its child widgets will be disabled.
When you want to re-enable them, you enable the frame.
Any widgets inside the frame which are already disabled won't be enabled when the frame is re-enabled, so you won't lose your enabled/disabled state on the individual buttons
Here is a simple example. Note that I've used explicit enable/disable buttons which act as a proxy for your timer.
#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QFrame>
int main(int argc, char** argv)
{
QApplication* app = new QApplication(argc, argv);
QMainWindow* window = new QMainWindow();
window->setFixedSize(1024, 200);
QWidget* widget = new QWidget();
QHBoxLayout layout(widget);
QPushButton* enable = new QPushButton("enable");
QPushButton* disable = new QPushButton("disable");
QFrame* frame = new QFrame();
layout.addWidget(enable);
layout.addWidget(disable);
layout.addWidget(frame);
QVBoxLayout frame_layout(frame);
for (int i = 0; i < 5; ++i)
frame_layout.addWidget(new QPushButton("click"));
// this shows that an already disabled button remains disabled
QPushButton* already_chosen = new QPushButton("click");
frame_layout.addWidget(already_chosen);
already_chosen->setEnabled(false);
QObject::connect(enable, &QPushButton::clicked, [&]{ frame->setEnabled(true); });
QObject::connect(disable, &QPushButton::clicked, [&]{ frame->setEnabled(false); });
window->setCentralWidget(widget);
window->show();
return app->exec();
}

How to enable mouse tracking on a QWindow

I'm using a QWindow (Not a QMainWindow) with OpenGL. I need to use a QWindow to correctly control the OGL context.
I'm trying to follow the Scribble example to implement something similar to panning, but I can't find a paradigmatic way to trigger the mouseMoveEvent().
How can I get a "tooltip" effect where mouseMoveEvent() is constantly triggered, similar to setMouseTracking()?
It works fine for me. I created a test program with a MainWindow that inherits QWindow instead of QMainWindow, and handles the mouse move event to print the cursor position:
void MainWindow::mouseMoveEvent(QMouseEvent *e)
{
qDebug("%d, %d", e->pos().x(), e->pos().y());
}
It works, as I move the mouse I get events even without pressing any mouse buttons.
If mouse tracking is disabled (the default), the widget only receives mouse move events when at least one mouse button is pressed while the mouse is being moved.
you can call hasMouseTracking() or setMouseTracking() to control mouse's tracing state.
When mouse is traced, mouseMoveEvent() will be called, and you can reimpletment mouseMoveEven() to acquire mouse position, just as #sashoalm did.
BTW, mouse event is always transfered to your app, but filtered by it's parent or itself. You can reimpletment eventFilter() to code your own filter.

Qt get mouse events outside of the application window

First I'm not certain this is even possible without some sort of hacking of X.11 input but the discussions i'd seen online made me think it was possible.
Allow me to explain what I hope to do. I want a Qt application which will most likely just be a small window that sides on the screen sort of like a widget. The application does nothing until the user drags another application window over the top of it. The way I was hoping to detect this was to track the mouse and see if the left click is down and the mouse is over the Qt window and Qt is not the active window then do some action. However currently I havent been able to get mouse events when my Qt application is not the active window. I think some of these posts I linked refer to 'window' as a QWindow inside the QApp.
What I mean by window however is a X.11 Window, any application opened in X. My screenshots I hope highlight my current plight. I've attached my code as well and am happy to take any suggestions. Any other hacks that are known to help me achieve this I would also appreciate being informed of.
The red shows where my cursor has clicked, and the mouse event is recorded outside of the Qt window. This was triggered by the 'FocusOut' event however and is the last event I have managed to detect.
As we can see in the console, the mouse has moved but no events are caught. I really want to detect when the mouse crosses over onto the position the Qt App Window is at regardless of whether it is on top of another window or not.
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::MouseMove)
{
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
statusBar()->showMessage(QString("Mouse move (%1,%2)").arg(mouseEvent->pos().x()).arg(mouseEvent->pos().y()));
qDebug() << QString::number(mouseEvent->pos().x());
qDebug() << QString::number(mouseEvent->pos().y());
}
if (event->type() == QEvent::FocusOut)
{
QFocusEvent *focusEvent = static_cast<QFocusEvent*>(event);
focusEvent->accept();
qDebug()<<"event Filter Mouse Move111"<<QCursor::pos();
}
return false;
}
void MainWindow::initWindow()
{
//Makes the window frameless and always on top
//setWindowFlags(Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint);
//Makes the window transparent
//setAttribute(Qt::WA_TranslucentBackground);
//Allows 'mouseMoved' events to be sent, not sure yet if this will be useful, I think we want mouseDragged
setMouseTracking(true);
grabMouse();
//setup this as an event filter for mouse events
qApp->installEventFilter(this);
}
Alright heres how I solved this problem. The event system in Qt, any application I assume, won't register events when the window is not active. However the process is obviously still running so data you can access while the window is active you can access whilst the window is no longer active.
Use a timed poll method to get the mouse position every n seconds
//Method used to hopefully track the mouse regardless of whether or not it is inside the active window
void MainWindow::pollMouse(unsigned long sec)
{
//Loop forever
while ( true )
{
QPoint mouseLoc = QCursor::pos();
qDebug() << "Mouse position global: x,y" << mouseLoc.x() << mouseLoc.y();
QThread::sleep(sec);
}
}

Qt how to set global mouseReleaseEvent when i have pile of widgets

is there away to set global mouseReleaseEvent?
what i mean is i have QMainWindow and on it QFrame and init QListView and In it
i have Widgets that constarct the QListView and inside the widget i have al sort of lables and text fields.
so i want to detect mouseRelease any where in my app i have to implement in all widgets the mouseReleaseEvent?
void ItemWidget::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
;
}
event->accept();
}
According to the QMouseEvent documentation, the widget that receives the mouse press will also get the mouse release. So, you shouldn't have to look any further for your mouse release than the widget that received the original press. This is usually referred to as a mouse "grab."
You may also want to check that Qt::WA_NoMousePropagation is not set on one of your children. If it is set, it will ensure that your mouse event does not bubble up.
If you really want to catch all mouse release events, you could try installing an event filter on the QApplication itself. That is a bit of a heavy solution, but you should get every mouse release event.
Looking at the (somewhat older) documentation here indicates that events are by default ignored by a widget and are propagated to their parents. So, if you make all your widgets children of your main application widget (or 'grandchildren', etc.) then you should only need to install the event handler on the application widget.