Change QWidget Parent During Mouse Event - c++

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!

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.

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

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.

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.

QGraphicsScene mouseMoveEvent does not work until QGraphicsView wheelEvent

I have a strange issue that I have been unable to determine the cause. Basically, I created a 2D view with pan and zoom functionality and a scene with items that can moved with grid snapping. To move an item in the scene I extended Scene::mousePressEvent to get a pointer to the item and Scene::mouseMoveEvent to keep the item tracked on the cursor. To drop the item, I used Scene::mousePressEvent again. To pan, I extended View::mousePressEvent, View::mouseReleaseEvent, and View::mouseMoveEvent and to zoom I extended View::wheelEvent.
Now for the symptoms:
I start the application with an item in the Scene. If I click and hold, then move the mouse, the item moves as intended. As soon as I release the mouse button, the item stops moving. I can click to drop and the item is placed according to the drop code in Scene::mousePressEvent. Try again and still the item only moves when the mouse button is pressed.
Then comes the strange part: If I use the mouse wheel to zoom the View, everything performs as expected after that event. The mouse is clicked to select an item, it moves as I move the mouse and drops when I click again.
So the obvious solution:
wheelEvent(new QWheelEvent(QPointF(0,0),0,Qt::NoButton,Qt::NoModifier));
called at the creation of the View and everything works fine. It calls the extended View::wheelEvent with no change to the view and before the scene is even created, but afterwards the programs behaves as expected.
So I'm here to see if any of the excellent Qt experts out there can explain this strange behavior. Any comments or direction are appreciated.
In case it helps, here is the View::wheelEvent override code. tform is a QTransform with which I maintain zoom. Also, I've tried with and without the call to the base method but there is no change in behavior.
void SchematicView::wheelEvent(QWheelEvent* event)
{
// Scale the view / do the zoom
double scaleFactor = 1.1;
if(event->delta() > 0 && tform.m11() < max_zoom) {
tform.scale(scaleFactor,scaleFactor);
} else if (event->delta() < 0 && tform.m11() > min_zoom){
tform.scale(1.0/scaleFactor,1.0/scaleFactor);
}
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
setTransform(tform);
QGraphicsView::wheelEvent(event);
}
Without a SSCCE to look at and test with, it's hard to say for sure, but what you're describing sounds a lot like your mouseMoveEvent() callback is only getting called when the mouse button is being held down during the move. That, in turn, sounds a lot like the expected behavior for mouseMoveEvent(), as documented in QWidget::mouseMoveEvent():
If mouse tracking is switched off, mouse move events only occur if a
mouse button is pressed while the mouse is being moved. If mouse
tracking is switched on, mouse move events occur even if no mouse
button is pressed.
If that is indeed the problem, then a call to setMouseTracking(true) may get you the behavior you are looking for.
On a broader level, note that there are easier ways of obtaining the behavior you are trying to implement -- for example, to allow the user to drag and drop items in a QGraphicsScene, all you really have to do is call setFlags(QGraphicsItem::ItemIsMovable) on any QGraphicsItems that you want the user to be able to drag around. No hand-coding of event handlers is necessary unless you are trying to obtain some non-standard behaviors.

Qt C++ Let multiple QGraphicsItem handle one mouse event

I've created an object Chartblock that implements QGraphicsItem. My goal is to create a grid of these objects, and when the mouse button is pressed (and held) and is drug over each block, perform something on each block as the cursor enters it.
Since a QGraphicsItem grabs the mouse events when it is clicked within it, other Items will not fire for the mouseMoveEvent. I then created an object based on the QGraphicsItemGroup to handle all the mouse events, but then I would need some way to pass mousePressEvent/mouseReleaseEvent as well as mouseMoveEvent to each child that the cursor is over.
Am I overthinking how to do this? It seems like such a simple action shouldn't be that difficult to create, but with QGraphicsItems holding onto the mouse events for itself, I'm not sure how to get around it. I've read similar situations, but nothing seems to give a straightforward answer.
Edit: I suppose a way to do this would keep track of the coordinates/sizes of every single QGraphicsItem I create in an array, then get the position of the cursor in the Group mouseMoveEvent, and see if there's a hit..
I was able to pull together a few similar answers to create a solution; I dropped the idea of placing all my QGraphicItem's in a Group, and placed them directly on a scene. With the scene grabbing all mouse events, I have the mouseMoveEvent check to see if the current position is on top of a QGraphicsItem - if so, perform something.
I still need to try and get itemAt() to work for my own classes that implement QGraphicsItem, as itemAt returns only QGraphicsItem's, but I'm sure some cast should get it working.
void ChartScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
QPointF mousePosition = event->scenePos();
QGraphicsItem* pItem = this->itemAt(mousePosition.x(), mousePosition.y());
}

Qt -- pass events to multiple objects?

I basically have 3 layers (Window > Scene > View) that each need to handle a mouseMove event without blocking the others. It seems only the youngest child is getting the event though. I was hoping I could process the event and then call event->ignore() to pass the event back up the stack, but it doesn't seem to be working.
Some relevant code if you need it:
void EditorWindow::createScene() {
m_scene = new EditorScene(this);
m_view = new EditorView(m_scene);
// ...
}
void EditorScene::mouseMoveEvent(QGraphicsSceneMouseEvent* mouseEvent) {
printf("B\n");
// ...
}
void EditorView::mouseMoveEvent(QMouseEvent* event) {
printf("C\n");
event->ignore();
}
Only "C" is being printed. Note that EditorScene and EditorView receive different types of mouse events so it's not completely trivial to pass them around.
The EditorWindow also needs the mouse coordinates; currently I'm sending a signal from one of the children which is caught by the window... but it shouldn't really be necessary to relay it that way, should it?
Found this nice article. Calling ignore() tells Qt to find another receiver. Sounds like it should work, but perhaps it means an unrelated receiver. The proper way to propagate it is actually to call BaseClass::Event like so:
void EditorView::mouseMoveEvent(QMouseEvent* event) {
QGraphicsView::mouseMoveEvent(event); // propogate to parent widget
printf("C\n");
}
Now it's printing BCBCBC... which is great, but I can't seem to nudge it up one more level...
Another edit: It was being propogated up properly, I just didn't have setMouseTracking enabled.
QGraphicsView::mouseMoveEvent(event);
Doesn't propagate up to the parent -- it actually propagates down to the scene.
Here is what's happens -- QGraphicsView receives QMouseEvent, translates it into QGraphicsSceneMouseEvent and passes it to the scene. Scene then passes it to appropriate item or, in your case, prints "B". Event handler then returns back to EditorView and prints "C".
Then, if you explicitly ignore event (mouse move is accepted by default), Qt event handler will pass the event to parent of EditorView. So try ignoring after you print "C".
Another thing about mouse move is this:
If mouse tracking is switched off, mouse move events only occur if a mouse button is pressed while the mouse is being moved. If mouse tracking is switched on, mouse move events occur even if no mouse button is pressed.
So make sure you have tracking enabled on parent of EditorView (or that you press buttons :)).
EDIT:
BTW, EditorScene is not a parent of EditorView. Well, it is in your code, but only in QObject meaning of parentship (memory management only).
QGraphicsScene and View don't have normal family relationship -- scene can have multiple views and those views are children of unrelated parents.
For window event propagation purposes you must have QWidget based parent. In fact, I'm pretty sure you reparent EditorView to EditorWindow, or one of its children (when you add it into layout).
INSTAEDIT:
For coordinates you want View itself to emit a signal. Both for decoupling reasons and because you probably want to show local coordinates of the view, and not of the parent window and not screen coordinates (right?). If you actually want scene coordinates, View is right choice too, because it knows transformation matrix.
Coordinates go like this:
Screen -> EditorWindow local -> EditorView local -> Scene transformed -> whatever item local transformed.
QGraphicsView::mousePressEvent( e ) in my mousePressEvent did the trick!