K. Barresi
K. Barresi

Reputation: 1315

Change QWidget Parent During Mouse Event

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.

Basic mouse handling overview:

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

Answers (3)

Michael Wilson
Michael Wilson

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

Dmitry Sazonov
Dmitry Sazonov

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

Marek R
Marek R

Reputation: 37512

Here is similar question.
So you suppose to use QDocWidget and enforce stacking of this widgets using tabifyDockWidget.

Upvotes: 0

Related Questions