Clicking links with the middle mouse button in Python3/Qt6 - pyqt6

I used a Python3 script under GTK, where I clicked on a link with the left mouse button to open it in the same window and on a middle mouse button to open it in a separate window.
I am now in the process of migrating this script from GTK to Qt6.
The problem is that I cannot intercept the middle mouse button in conjunction with a clicked link:
My browser window is based on QWebEngineView, I can intercept mouse button clicks with an event filter (eventFilter()), and I can intercept clicked links within the acceptNavigationRequest() function of a QWebEnginePage.
The URL of the clicked link is only available in acceptNavigationRequest(), but this function is only called by Qt6 when I use the left mouse button, not, as desired, the middle mouse button (neither does control + left mouse button work).
How can I let a middle mouse button click be recognized in a navigation request?
Debian 11.6 Bullseye, Python 3.9.2, PyQt6-Qt6 6.4.1

With the help of #musicamante (note the comments in the original post), I solved my problem this way:
g_mouse_button = '--'
class MyWebEngineView(QWebEngineView):
def __init__(self):
...
class MyEventFilter(QObject):
def eventFilter(self, obj, event):
global g_mouse_button
...
if event.button() == Qt.MouseButton.MiddleButton:
g_mouse_button = 'middle'
...
...
class MyWebEnginePage(QWebEnginePage):
def acceptNavigationRequest(self, url, ttype, isMainFrame):
global g_mouse_button
if ttype == QWebEnginePage.NavigationType.NavigationTypeLinkClicked and \
g_mouse_button == 'middle':
<open link in new window>
g_mouse_button = '--'
return False # ignore
return True # process
def createWindow(self, window_type):
return self
if __name__ == "__main__":
...
event_filter = MyEventFilter()
g_app.installEventFilter(event_filter)
...

Related

PyQt5 Hover over QPushButton with mouse pressed down

I am trying to simulate a paint brush with Qt buttons instead of pixels. I have overloaded the event filter on each button and can detect eventHover or eventFilter only when the mouse is not pressed down. I want to hold mouse click down and detect if i collide with a button. Any suggestions as to what i should do from here?
def eventFilter(self, a0, a1):
if a1.type() == QtCore.QEvent.Enter:
if(self.mouse.pressed):
ui_logic.square_press(self,"red")
return super().eventFilter(a0, a1)
def square_press(button,color):
style = "QPushButton{background-color:" + color + "}"
button.setStyleSheet(style)
Thank you
What i ended up doing is tracking the mouse and getting the widget at a location.
Example code:
class TrackMouse(QtWidgets.QWidget):
pressed = False
def __init__(self,obj,app):
self.app = app
super(QtWidgets.QWidget, self).__init__()
def eventFilter(self, a0, event):
if event.type() == QtCore.QEvent.MouseMove:
if(event.buttons() == QtCore.Qt.MouseButton.LeftButton):
widget = self.app.widgetAt(event.globalPos())
print(widget.__class__)
if(widget.__class__ is CustomButtom):
ui_logic.square_press(widget,"red")
return super().eventFilter(a0, event)

How to allow circular navigation of items in a QComboBox?

The Qt desktop app I'm writing contains a QCombobox in the UI (made with Designer). After I select the QCombobox, I can change the selected item by scrolling the mouse wheel, or by pressing the up/down arrows on the keyboard. That all works fine.
When navigating using the keyboard down arrow, for example, when I reach the bottom item in the list, the down arrow no longer changes the selected item. I understand that this is the expected behavior.
But for this particular QComboBox I'd like to be able to keep pressing the down arrow after reaching the final item in the list, and "wrap" back to the first item, so I can continue to cycle through the items. I have studied the documentation for QComboBox at https://doc.qt.io/qt-5/qcombobox.html and for QAbstractItemModel at https://doc.qt.io/qt-5/qabstractitemmodel.html, but I could not discover any way to achieve what I want here.
Ideally I'd prefer a solution that works for keyboard arrow navigation, for mouse scroll wheel navigation, and for any other UI gesture that might try to activate the "next" or "previous" item in the QComboBox.
I didn't try this solution, but I'm guessing that it's right by intuition.
I think you need to do:
Override keyPressEvent(QKeyEvent *e) to detect up and down arrows.
If the down arrow is pressed, check if it's the last index using currentIndex() const function, compared to the size of the combo box itself.
If so, change the current index to the first one using setCurrentIndex(int index).
Do the same for up arrow if you reached the first index.
P.S. As currentIndex() returned the index after pressing, this might make it jump from the penultimate index to the first one. Thus, I suggest using a private boolean member to be toggled when the condition is met for the first time.
I hope this solution helps you.
The full solution to this problem has a few different aspects.
When the QComboBox is expanded to show all the items, an elegant semantic solution is to override the QAbstractItemView::moveCursor() method. This part of the solution does not require low level event handlers because moveCursor() encapsulates the concept of "next" and "previous". Sadly this only works when the QComboBox is expanded. Note that the items are not actually activated during navigation in this case, until another gesture like a click or enter occurs.
When the QComboBox is collapsed to show one item at a time (the usual case), we have to resort to the low level approach of capturing each relevant gesture, as sketched in the answer by Mohammed Deifallah. I wish Qt had a similar abstraction here analogous to QAbstractItemView::moveCursor(), but it does not. In the code below we capture key press and mouse wheel events, which are the only gestures I'm aware of at the moment. If other gestures are also needed, we would need to independently implement each one. Because the Qt architects did not generalize the concepts of "next" and "previous" for these cases the way they did for QAbstractItemView::moveCursor().
The following code defines a replacement class for QComboBox that implements these principles.
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
# CircularListView allows circular navigation when the ComboBox is expanded to show all items
class CircularListView(QtWidgets.QListView):
"""
CircularListView allows circular navigation.
So moving down from the bottom item selects the top item,
and moving up from the top item selects the bottom item.
"""
def moveCursor(
self,
cursor_action: QtWidgets.QAbstractItemView.CursorAction,
modifiers: Qt.KeyboardModifiers,
) -> QtCore.QModelIndex:
selected = self.selectedIndexes()
if len(selected) != 1:
return super().moveCursor(cursor_action, modifiers)
index: QtCore.QModelIndex = selected[0]
top = 0
bottom = self.model().rowCount() - 1
ca = QtWidgets.QAbstractItemView.CursorAction
# When trying to move up from the top item, wrap to the bottom item
if index.row() == top and cursor_action == ca.MoveUp:
return self.model().index(bottom, index.column(), index.parent())
# When trying to move down from the bottom item, wrap to the top item
elif index.row() == bottom and cursor_action == ca.MoveDown:
return self.model().index(top, index.column(), index.parent())
else:
return super().moveCursor(cursor_action, modifiers)
class CircularCombobox(QtWidgets.QComboBox):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
view = CircularListView(self.view().parent())
self.setView(view)
def _activate_next(self) -> None:
index = (self.currentIndex() + 1) % self.count()
self.setCurrentIndex(index)
def _activate_previous(self):
index = (self.currentIndex() - 1) % self.count()
self.setCurrentIndex(index)
def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
if event.key() == Qt.Key_Down:
self._activate_next()
elif event.key() == Qt.Key_Up:
self._activate_previous()
else:
super().keyPressEvent(event)
def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
delta = event.angleDelta().y()
if delta < 0:
self._activate_next()
elif delta > 0:
self._activate_previous()

How to receive dropEvent when not a top level widget?

Edit: this issue seems to be specific to Qt version 5.12.0. See the answers for more details and for a workaround
I'm trying to implement a drop zone for loading files into my application.
It works when I just show the widget as a top level widget, but it stops working as soon as I include it into another parent widget.
The problem is, altough I'm receiving dragEnterEvent and accepting it, I never see the dropEvent.
This is my widget:
class FileDropZone(qt.QLabel):
"""Target area for a drag and drop operation"""
height = 33
def __init__(self, text="Add file", parent=None):
super().__init__(text, parent)
stylesheet = """
QLabel {
border: 2px dotted #B4BDBA;
qproperty-alignment: AlignCenter;
}
"""
self.setStyleSheet(stylesheet)
self.setAcceptDrops(True)
self.setFixedHeight(self.height)
def dragEnterEvent(self, event):
print("in drag enter event")
if event.mimeData().hasUrls():
print("hasUrls()")
event.acceptProposedAction()
def dropEvent(self, event):
print("in drop event")
urls = event.mimeData().urls()
for url in urls:
print(url.isLocalFile(), url.toLocalFile())
This is how I manage to make it work:
app = qt.QApplication([])
a = FileDropZone()
a.show()
app.exec_()
And this is the example where it does not work (dragEnter works, both prints are properly printed, but dropEvent does not print anything):
app = qt.QApplication([])
a0 = qt.QWidget()
l = qt.QHBoxLayout(a0)
a1 = FileDropZone("drop here", a0)
l.addWidget(a1)
a0.show()
app.exec_()
Any clues about what is broken? Does the parent need to forward the event, and if so, how should I implement it?
It looks like it is a bug which was introduced in Qt 5.12.0 and will be fixed in Qt 5.12.1, see this discussion and this bug report.
In the meantime:
The problem can be worked around by reimplementing dragMoveEvent() and accepting the event there too.
i.e. add e.g. the following method to the FileDropZone class:
def dragMoveEvent(self, event):
print("in drag move event")
if event.mimeData().hasUrls():
print("hasUrls()")
event.acceptProposedAction()

How should viewportEvent be implemented in a QAbstractScrollArea?

I'm having a lot of problems getting the details right for my QAbstractScrollArea. This is my current implementation of viewportEvent:
def viewportEvent(self, event):
if event.type() in [QEvent.MouseButtonPress,
QEvent.MouseMove,
QEvent.MouseButtonRelease,
QEvent.ContextMenu,
QEvent.KeyPress,
QEvent.KeyRelease]:
return self.my_viewport.event(event)
if event.type() == QEvent.Resize:
self.my_viewport.resizeEvent(event)
return super().viewportEvent(event)
if event.type() in [QEvent.UpdateLater,
QEvent.UpdateRequest]:
self.my_viewport.event(event)
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
The idea is to pass through (to the viewport widget) things like key and mouse presses. Resize events need to be passed through and sent to the abstract-scroll-area itself? What about the size for the scroll bars? Shouldn't the resize event's size be changed. If I don't pass paint events through, the viewport widget doesn't paint.
Minimum working example of broken QOpenGLWidget with QAbstractScrollArea:
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import (QAbstractScrollArea, QApplication, QMainWindow,
QOpenGLWidget)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.scope_view_widget = ScrollingScopeView()
self.setCentralWidget(self.scope_view_widget)
class ScopeView(QOpenGLWidget):
def paintGL(self):
super().paintGL()
print("Painting")
class ScrollingScopeView(QAbstractScrollArea):
def __init__(self):
super().__init__()
self.set_my_viewport(ScopeView())
def set_my_viewport(self, new_viewport):
self.my_viewport = new_viewport
self.setViewport(self.my_viewport)
def viewportEvent(self, event):
# Uncommenting this breaks painting.
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
application = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(application.exec_())
Filed as a Qt bug: https://bugreports.qt.io/browse/QTBUG-53269
My previous answer was absolutely wrong. Kudos to the OP for his investigation.
Per the docs:
When inheriting QAbstractScrollArea, you need to do the following:
Control the scroll bars by setting their range, value, page step, and tracking their movements.
Draw the contents of the area in the viewport according to the values of the scroll bars.
Handle events received by the viewport in viewportEvent() - notably resize events.
Use viewport->update() to update the contents of the viewport instead of update() as all painting operations take place on the viewport.
Unless you need to do other event management, the very short viewportEvent() in your MCVE is correct. Take a look at the code (a better look than I did) and you'll see that most events (including paint events) are not passed to the viewport. Curiously, the code does make an exception to properly resize QOpenGLWidget.
I realize now the logic behind not painting by default is to allow you to update only the region of the viewport currently visible.
In short, the below is fine. I would recommend checking to ensure the paint event only includes the rect currently visible (check the value of rect() in the paint event), otherwise you'll be painting areas not currently visible in your viewport.
def viewportEvent(self, event):
# Uncommenting this breaks painting.
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
Apologies for my screwup. I hope this is helpful.

How to draw over QLabel in Qt

I have to create one screen in Qt in which I have to show a remote having lots of buttons in it and when user clicks some button on actual remote, corresponding button in the image get highlighted. So what I have done is, I have used QLabel and set the remote image as background image and then I have put small rectangular label for each button and filled them with semi transparent color and when user click button in actual remote label color changes, but by using this method lot of labels are getting used making code looking inefficient, so I was thinking of drawing on QLabel (which has a remote as background image) over buttons.
Can anybody suggest me, which API of Qt should I use, and how to follow up on this?
I believe QGraphics is the correct route for a completely custom graphical interface, but if you want to try something that doesn't require you to change too much of your existing approach, you can do a widget with a custom paint event:
This is written in PyQt but you can easily translate to Qt
from PyQt4 import QtCore, QtGui
class LabelButton(QtGui.QWidget):
clicked = QtCore.pyqtSignal()
def __init__(self, labelStr, pixStr, parent=None):
super(LabelButton, self).__init__(parent)
self.label = labelStr
self.pix = QtGui.QPixmap(pixStr)
def paintEvent(self, event):
super(LabelButton, self).paintEvent(event)
rect = event.rect()
painter = QtGui.QPainter(self)
painter.drawPixmap(rect, self.pix)
pos = (rect.bottomLeft()+rect.bottomRight()) / 2
pos.setY(pos.y()-10)
painter.drawText(pos, self.label)
painter.end()
def mousePressEvent(self, event):
event.accept()
self.clicked.emit()
def handleClick():
print "CLICK"
if __name__ == "__main__":
app = QtGui.QApplication([])
widget = LabelButton("A Text Label", "myImage.png")
widget.resize(600,400)
widget.show()
widget.raise_()
widget.clicked.connect(handleClick)
app.exec_()
This is a rough example. You can get more fine tuned with the drawing of the text. This widget takes a label string, and a picture path, and will paint the picture as the background, and the text as a label. You can do any number of things with this custom widget in both the paint event, and with custom signals and events.
I have used this code to Draw over Image in Label:
Image is loaded in Ui and the Code is as follows In paintevent
void ColorTab::paintEvent(QPaintEvent *e){
ui->lbl_capture_img->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
ui->Lbl_color_tab_WG->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
Cap_Image = QImage(ui->lbl_capture_img->pixmap()->toImage());
Lbl_Image = QImage(ui->Lbl_color_tab_WG->pixmap()->toImage());
QPainter painter_Lbl(&Lbl_Image);
QPainter painter_Cap(&Cap_Image);
QPen pen(Qt::white, 5, Qt::DotLine, Qt::RoundCap, Qt::RoundJoin);
painter_Lbl.setPen(pen);
painter_Cap.setPen(pen);
painter_Lbl.drawPolygon(blinkRect_Lbl);
painter_Cap.drawPolygon(blinkRect_Cap);
ui->lbl_capture_img->setPixmap(QPixmap::fromImage(Cap_Image));
ui->Lbl_color_tab_WG->setPixmap(QPixmap::fromImage(Lbl_Image));
painter_Cap.end();
painter_Lbl.end();
}

Resources