Trouble with KDE/Plasma window movement/snapping behavior
-
I'm having this problem with Pyside6, but I suspect the same would affect a C++ program.
I have a small frameless (Qt.FramelessWindowHint) window populated with a single QLabel displaying an image. I intend for this window to be movable by clicking anywhere on it and dragging.
On most systems, that's not done by default, so I added handlers for mouse press/move/release on the window to emulate it moving. That works correctly.
However, in at least some KDE/Plasma configurations (including mine), behavior similar to this is implemented by the window manager, along with window-to-window edge snapping. That would be fine, except my cross-platform manual movement conflicts with it.
The KDE/Plasma move events cause the window to snap; however, my own do not, and it seems my own come after the KDE/Plasma on release. The result is my window LOOKS snapped, but then it jolts out of place. It also seems like there is some kind of disagreement over where the window should go beyond just the snapping, as the window moves on release even if it doesn't snap before or after release.
I have been largely unsuccessful finding information about this online, other than one Stack Overflow post essentially saying the behavior exists, which I already knew. However, through experimentation, I found the following:
- Window flag Qt.Popup stops the default movement and snapping, but as a result, my window behaves differently to all the others on the system (it's the only one that WON'T snap at all).
- Window flag Qt.X11BypassWindowManagerHint functionally does the same as Popup for my purposes, but with more side-effects.
- Sticking in an event filter to see if I could tell which events were which showed that my own move events and the ones originating from the window manager looked effectively identical.
- "accept()"ing the various mouse events doesn't stop the KDE behavior.
- Implementing moveEvent() and having it do nothing other than accept() the event didn't seem to do anything.
What I want: my window to behave like all other windows within the window manager (with or without snapping per window manager rules), but WITH the click-anywhere-and-drag movement behavior, and without weird secondary snapping/moving to the wrong location on mouse release.
I think if I could just identify if this behavior exists or not in the given window manager, I would be fine. But it seems like there is no mechanism to do that. I can't go off the window manager being KDE/Plasma alone because it isn't universal behavior, it's configurable.
Here's a cut down version of my window and how it's made. The program is quite a bit more complicated, but the full thing is here if you want to see it.
class Viewer(QMainWindow): def __init__(self, app, filepath): super().__init__(None) self.setWindowFlag(Qt.FramelessWindowHint) # self.setWindowFlag(Qt.Popup) # Bypass KDE movement and snapping if present ... self.drag_down_window_offset = None ... self.label = QLabel(self) self.setCentralWidget(self.label) ... def updateQtImage(self): pixmap = QPixmap.fromImage(self.image_layers.getQtImage(self.current_frame)) first_update = self.label.pixmap is None self.label.setPixmap(pixmap) previous_size = self.size() # Needed to keep the image in its current size from blocking shrinking the window. self.setMinimumSize(0, 0) # Undo the max size that would be set below from a previous update. self.setMaximumSize(self.maximumWidth(), self.maximumHeight()) self.resize(pixmap.width(), pixmap.height()) # Prevent maximize or other window size changes. self.setMinimumSize(pixmap.width(), pixmap.height()) self.setMaximumSize(pixmap.width(), pixmap.height()) new_size = self.size() if not first_update: resize_offset = (previous_size - new_size) / 2 self.move(self.pos() + QPoint(resize_offset.width(), resize_offset.height())) ... def mousePressEvent(self, event): if event.button() is Qt.MouseButton.LeftButton and Qt.ControlModifier in event.modifiers(): ... elif event.button() is Qt.MouseButton.LeftButton: self.drag_down_window_offset = self.pos() - event.globalPosition().toPoint() def mouseMoveEvent(self, event): if self.drawing_crop: pos = event.position().toPoint() self.rubber_band.setGeometry(QRect(self.drawing_crop_origin, pos).normalized()) elif self.drag_down_window_offset: self.move(self.drag_down_window_offset + event.globalPosition().toPoint()) def mouseReleaseEvent(self, event): if event.button() is Qt.MouseButton.MiddleButton: self.close() elif event.button() is Qt.MouseButton.LeftButton and self.drawing_crop: ... elif event.button() is Qt.MouseButton.LeftButton and Qt.AltModifier in event.modifiers(): self.resetImageScale() elif event.button() is Qt.MouseButton.LeftButton and self.drag_down_window_offset: self.drag_down_window_offset = None ... class ViewerApplication(QApplication): def __init__(self, args): super().__init__(args) # Does nothing relevant ... def openImageFile(self, path): viewer = Viewer(self, path) viewer.setWindowIcon(self.application_icon) viewer.show() # Sidestep the window getting "stuck" to the screen edges if too large. # See: https://stackoverflow.com/a/68477844 viewer.updateQtImage() viewer.activateWindow() self.windows.append(viewer)