Reputation: 1315
I'm trying to create a detachable type style widget, like in the way Chrome tabs are detachable (class is called Tab). I have everything working, except for a bug where sometimes (maybe 50% of the time), the Tab object never gets the mouse release event, and stops getting mouse move events.
Essentially, the detaching system works by allowing drags in the mouse press/move/release functions, just like normal. The mouseMoveEvent
checks the total distance moved from the start, and if over a certain amount, will start the "detaching" process. The detaching process involves setting the parent widget to 0 (top level widget, undecorated window), so the Tab object is pretty much floating above everything, under the mouse, and continues to be dragged along with it until released.
I ran through all the QEvent
items being delivered, and I found that when this issue occurs, the QEvent::MouseMove items (and all mouse events after this) are being sent to the TabBar (the Tab object's original parent). This occurs directly after calling setParent(0)
on the Tab.
void Tab::mousePressEvent(*) {
[set up some boolean, start positions, etc]
}
void Tab::mouseMoveEvent(*) {
[track the updated position]
if (positionChange > STATIC_AMOUNT)
detachTab();
}
void Tab::mouseReleaseEvent(*) {
[return the Tab to its original position, and set the parent back to the TabBar]
}
void Tab::detachTab() {
QPoint mappedPos = mapToGlobal(0, 0);
setParent(0); //The loss of MouseMove events occurs when this returns.
move(mappedPos);
show();
raise();
}
Here are the events that the Tab object receives (first row is QEvent type, second is the name)
[Tab::detachTab() started]
[setParent(0) started]
QEvent::Hide
QEvent::Leave
qApp QEvent::MouseMove [ TabBar ] <-- now the TabBar is soaking up the mouse events
QEvent::HideToParent
QEvent::ParentAboutToChange
QEvent::ParentChange
[setParent(0) returned]
....
Summed up: my draggable QWidget loses QEvent::MouseMove and QEvent::MouseButtonRelease events after having its parent set to 0.
Any advice would be really appreciated!
Upvotes: 4
Views: 1133
Reputation: 860
I had this problem, or one very similar to it:
// viewport_ is leaving the main window, reparent it
QPoint lastPos = viewport_->pos();
QPoint windowPos = oldViewportParent_->mapToGlobal( QPoint( 0,0 ) );
lastPos += windowPos;
viewport_->setParent( NULL );
viewport_->window()->move( lastPos );
viewport_->window()->setWindowTitle( title_ );
viewport_->show(); // Mouse events STOP.
At that point it would stop receiving mouse events until you either released the mouse, or moused back over the widget.
To solve this, I used QApplication::qInstallMessageHandler to capture Qt logging, and exported QT_LOGGING_RULES:
export QT_LOGGING_RULES="*.debug=true;qt.qpa.input.mouse=true;qt.qpa.cocoa.notifications=true;qt.qpa.input.events=true;qt.nativeinterface=true;qt.qpa.input.devices=true"
What I found was that, as expected, Qt was re-parenting the Widget's window - actually a Cocoa window. It's didn't occur to me at the time that it was strange that the widget already had a Cocoa window, since it was just a widget (or so I thought)
Then I wrote a tiny toy application to do the same thing, and, of course it didn't fail.
But, in the toy application, when the newly parented widget was shown, Qt actually creating a Cocoa window and "porting" the QWidget to it.
Sooo, lots of debugging and breakpoints in qwindow.cpp and qwidget.cpp later.
It turned out at some point after the QWidget (viewport_) was calling QWidget::winId(). At that point, Qt would internally invoke QWidgetPrivate::createWinId(), which would create a Cocoa window for the QWidget, even though it was just a plain widget.
Attempts to move that widget out in the code above resulted in the mouse tracking problem.
If I either eliminated that call (it was just for logging), or called, instead, QWidget::window()::winId (which is correct) solved the problem.
Apparently calling winId() on a widget causes Qt to create a Cocoa Window, which may not be wrong, but has other side affects which led to this bug.
So the moral of the story is Don't Call QWidget::winId(), call QWidget::window::winId() instead.
P.S. Here's my message logger:
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
QByteArray localMsg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
const char *filename = (strrchr(file, '/') ? strrchr(file, '/') + 1 : file);
switch (type) {
case QtDebugMsg:
qDebug( "%s:%d %s Debug: %s",
filename, context.line, function, localMsg.constData());
break;
case QtInfoMsg:
qDebug( "%s:%d %s Info: %s",
filename, context.line, function, localMsg.constData());
break;
case QtWarningMsg:
qDebug( "%s:%d %s Warning: %s",
filename, context.line, function, localMsg.constData());
break;
case QtCriticalMsg:
qDebug( "%s:%d %s Critical: %s",
filename, context.line, function, localMsg.constData());
break;
case QtFatalMsg:
qDebug( "%s:%d %s Fatal: %s",
filename, context.line, function, localMsg.constData());
break;
default:
assert("Unknown Message Type");
}
}
Upvotes: 0
Reputation: 8994
A bit tricky workaround. I didn't test it, it's just an idea.
When your mouse hovers draggable part of a widget you may create topmost widget (let's call it Shade) with Qt::FramelessWindowHint
(and possible with Qt::WA_TranslucentBackground
). You may manipulate with Shade apperance via reimplementing paintEvent
. For example - draw content of original widget, or draw some transparent preview, etc.
Then you may resize a Shade during dragging, to show user that widget will be detached. You will not loose mouse capture.
When user release mouse - you remember position of Shade, destroy it and detach+move original widget.
Feel free to ask, if you want more details.
Upvotes: 2
Reputation: 37512
Here is similar question.
So you suppose to use QDocWidget and enforce stacking of this widgets using tabifyDockWidget.
Upvotes: 0