PyQt mouse events for QTabWidget - python

I want to detect middle mouse clicks on a QTabWidget. I was expecting there to be a mouse event related signal on QWidget, but all I am seeing are methods.
Do I need to subclass the QTabWidget and then override said methods in order to do what I want, or am I missing something?

You can either install an event filter on the QTabBar (returned by QTabWidget.tabBar()) to receive and handle press and release events, or subclass QTabBar to redefine mousePressEvent and mouseReleaseEvent and replace the QTabBar of the QTabWidget with QTabWidget.setTabBar().
Example using the event filter:
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow,self).__init__()
self.tabWidget = QTabWidget(self)
self.setCentralWidget(self.tabWidget)
self.tabWidget.tabBar().installEventFilter(self)
self.tabWidget.tabBar().previousMiddleIndex = -1
def eventFilter(self, object, event):
if object == self.tabWidget.tabBar() and \
event.type() in [QEvent.MouseButtonPress,
QEvent.MouseButtonRelease] and \
event.button() == Qt.MidButton:
tabIndex = object.tabAt(event.pos())
if event.type() == QEvent.MouseButtonPress:
object.previousMiddleIndex = tabIndex
else:
if tabIndex != -1 and tabIndex == object.previousMiddleIndex:
self.onTabMiddleClick(tabIndex)
object.previousMiddleIndex = -1
return True
return False
# function called with the index of the clicked Tab
def onTabMiddleClick(self, index):
pass
Example using a QTabBar subclass:
class TabBar(QTabBar):
middleClicked = pyqtSignal(int)
def __init__(self):
super(QTabBar, self).__init__()
self.previousMiddleIndex = -1
def mousePressEvent(self, mouseEvent):
if mouseEvent.button() == Qt.MidButton:
self.previousIndex = self.tabAt(mouseEvent.pos())
QTabBar.mousePressEvent(self, mouseEvent)
def mouseReleaseEvent(self, mouseEvent):
if mouseEvent.button() == Qt.MidButton and \
self.previousIndex == self.tabAt(mouseEvent.pos()):
self.middleClicked.emit(self.previousIndex)
self.previousIndex = -1
QTabBar.mouseReleaseEvent(self, mouseEvent)
class MainWindow(QMainWindow):
def __init__(self):
super(QMainWindow,self).__init__()
self.tabWidget = QTabWidget(self)
self.setCentralWidget(self.tabWidget)
self.tabBar = TabBar()
self.tabWidget.setTabBar(self.tabBar)
self.tabBar.middleClicked.connect(self.onTabMiddleClick)
# function called with the index of the clicked Tab
def onTabMiddleClick(self, index):
pass
(In case you wonder why there is so much code for such a simple task, a click is defined as a press event followed by a release event at roughly the same spot, so the index of the pressed tab has to be the same as the released tab).

Related

Make a QSpinBox which requires a double-click to edit

I would like to make a spin-box which only is editable after double-clicking in the digit display area.
My attempt below disables the focus in all cases except when the increment/decrement buttons are pressed.
I want increment/decrement to perform the actions without stealing the focus.
I do want the the normal blinking cursor and edit functionality when the text area is double-clicked.
After editing, the widget should release focus when another widget is clicked, or enter is pressed.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
event_dict = {v: k for k, v in QtCore.QEvent.__dict__.items() if isinstance(v, int)}
noisy_events = [
'Paint',
'Show',
'Move',
'Resize',
'DynamicPropertyChange',
'PolishRequest',
'Polish',
'ChildPolished',
'HoverMove',
'HoverEnter',
'HoverLeave',
'ChildAdded',
'ChildRemoved',
]
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
if a1.type() == QtCore.QEvent.FocusAboutToChange:
print("intercepted focus about to change")
return True
if a1.type() == QtCore.QEvent.FocusIn:
print("intercepted focus in")
return True
if a1.type() == QtCore.QEvent.MouseButtonPress:
print("intercepted mouse press")
#return True
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
print("intercepted double click")
self.setFocus()
else:
if a1.type() in event_dict:
evt_name = event_dict[a1.type()]
if evt_name not in noisy_events:
print(evt_name)
else:
pass
#print(f"Unknown event type {a1.type()}")
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
l.addWidget(ClickableSpinBox())
l.addWidget(ClickableSpinBox())
l.addWidget(QtWidgets.QDoubleSpinBox())
w.setLayout(l)
w.show()
app.exec_()
Edit:
To let the mouse-scroll function and the increase/decrease buttons working
I make the QLineEdit inside of the QDoubleSpinBox to be enabled/disabled when you double click inside it or in the borders of the SpinBox. With this, you can still change the value inside it with the mouse-scroll or with the buttons. Here is your code modified:
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When double clicked inside the Disabled QLineEdit of
## the SpinBox, this will enable it and set the focus on it
self.lineEdit().setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will diable the QLineEdit
self.lineEdit().setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the QLineEdit
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.lineEdit().setEnabled(False)
return super().eventFilter(a0, a1)
def stepBy(self, steps):
## The reason of this is because if you click two consecutive times
## in any of the two buttons, the object will trigger the DoubleClick
## event.
self.lineEdit().setEnabled(False)
super().stepBy(steps)
self.lineEdit().deselect()
The result with the QLineEdit disabled and the buttons enabled:
To let ONLY the mouse-scroll function
You just have to remove the buttons from the code above, using setButtonSymbols().
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.lineEdit().setEnabled(False)
self.setFocusPolicy(Qt.NoFocus)
## Changing button's symbol to 2 means to "delete" the buttons
self.setButtonSymbols(2)
The result with the buttons "disabled":
Previous Answer (before the Edit)
I have a tricky solution, and it consists of Enable/Disable the customs spin boxes you created. With this, the spinboxes will be enabled (and editable) only when you double-clicked on them, and when you lose focus on them they will be disabled automatically, passing the focus to the enabled SpinBox.
The reason I did that is that when the SpinBox is enabled, the DoubleClick event will only be triggered when you double click on the borders or on the increment/decrement buttons. Disabling them will do the trick because the double click event will be triggered wherever you press inside the SpinBox.
Here is your code with my modifications: (there are comments inside te code to help you understand what I did)
class ClickableSpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.installEventFilter(self)
self.setFocusPolicy(Qt.NoFocus)
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if a0 is not self:
return super().eventFilter(a0, a1)
elif a1.type() == QtCore.QEvent.MouseButtonDblClick:
## When you double click inside the Disabled SpinBox
## this will enable it and set the focus on it
self.setEnabled(True)
self.setFocus()
elif a1.type() == QtCore.QEvent.FocusOut:
## When you lose the focus, e.g. you click on other object
## this will disable the SpinBox
self.setEnabled(False)
elif a1.type() == QtCore.QEvent.KeyPress:
## When you press the Enter Button (Return) or the
## Key Pad Enter (Enter) you will disable the SpinBox
if a1.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
self.setEnabled(False)
return super().eventFilter(a0, a1)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
w = QtWidgets.QWidget()
l = QtWidgets.QHBoxLayout()
## I store the SpinBoxes to give the disable property after
## generating its instance
sp1 = ClickableSpinBox()
sp1.setEnabled(False)
sp2 = ClickableSpinBox()
sp2.setEnabled(False)
sp3 = QtWidgets.QDoubleSpinBox()
l.addWidget(sp1)
l.addWidget(sp2)
l.addWidget(sp3)
w.setLayout(l)
w.show()
app.exec_()
And some screenshots of that code running:
The demo script below should do everything you want. I have added two extra features: (1) disabling of text selection, and (2) disabling of mouse-wheel increments on the text-box (but not the buttons). If these aren't to your taste, they can easily be adapted or removed (see the comments in the code). The implementation is otherwise very simple, since it does not rely on controlling the focus.
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ClickableSpinBox(QDoubleSpinBox):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setEditingDisabled(True)
self.lineEdit().installEventFilter(self)
self.editingFinished.connect(self.setEditingDisabled)
def editingDisabled(self):
return self.lineEdit().isReadOnly()
def setEditingDisabled(self, disable=True):
self.lineEdit().setReadOnly(disable)
self.setFocusPolicy(Qt.TabFocus if disable else Qt.WheelFocus)
# optional: control selection in text-box
if disable:
self.clearSelection()
self.lineEdit().selectionChanged.connect(self.clearSelection)
else:
self.lineEdit().selectionChanged.disconnect(self.clearSelection)
self.lineEdit().selectAll()
def clearSelection(self):
self.lineEdit().setSelection(0, 0)
def eventFilter(self, source, event):
if (event.type() == QEvent.MouseButtonDblClick and
source is self.lineEdit() and self.editingDisabled()):
self.setEditingDisabled(False)
self.setFocus()
return True
return super().eventFilter(source, event)
# optional: control mouse-wheel events in text-box
def wheelEvent(self, event):
if self.editingDisabled():
self.ensurePolished()
options = QStyleOptionSpinBox()
self.initStyleOption(options)
rect = self.style().subControlRect(
QStyle.CC_SpinBox, options,
QStyle.SC_SpinBoxUp, self)
if event.pos().x() <= rect.left():
return
super().wheelEvent(event)
def keyPressEvent(self, event):
if not self.editingDisabled():
super().keyPressEvent(event)
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.spinboxA = ClickableSpinBox()
self.spinboxB = ClickableSpinBox()
self.spinboxC = QDoubleSpinBox()
layout.addWidget(self.spinboxA)
layout.addWidget(self.spinboxB)
layout.addWidget(self.spinboxC)
self.setFocusPolicy(Qt.ClickFocus)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.setGeometry(900, 100, 200, 100)
window.show()
sys.exit(app.exec_())

Creating right-click menu on splitter handle in PyQt5 python

I have made a splitter. I have been trying to bring a right-click menu when the splitter handle is right-clicked.
Here is the code I have created. Currently, it doesn't recognise the right clicks on the handle of the splitter. (note: this code currently updates count if right-clicked in the first frame of the splitter i,e Top left frame)
from PyQt5.QtWidgets import *
from PyQt5 import QtGui
from PyQt5.QtCore import Qt
# This class is to create the outer window
class OuterLayout(QMainWindow):
def __init__(self):
super().__init__()
self.window()
def window(self):
self.setMinimumSize(1000, 900)
self.showMaximized()
self.setWindowIcon(QtGui.QIcon('Images/Logo_small.png'))
self.setWindowTitle('Splitter')
self.menu_bar()
inner_layout = SplitterLayout()
layout = inner_layout.add_layout()
self.setCentralWidget(layout)
def menu_bar(self):
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu('File')
self.file(file_menu)
edit_menu = menu_bar.addMenu('Edit')
self.edit(edit_menu)
def file(self, file):
new = QAction('New', self)
file.addAction(new)
def edit(self, edit):
pass
# This class creates the splitter window
class SplitterLayout(QWidget):
def __init__(self):
super(QWidget, self).__init__()
self.count = 0
self.splitter_handle_width = 3 # This is to set the width of the handle
# This is a method to add a new splitter window
def add_layout(self):
left = QFrame()
left.setFrameShape(QFrame.StyledPanel)
bottom = QFrame()
bottom.setFrameShape(QFrame.StyledPanel)
splitter1 = QSplitter(Qt.Horizontal)
splitter1.setHandleWidth(self.splitter_handle_width)
lineedit = QLineEdit()
lineedit.setStyleSheet('background-color:green')
splitter1.addWidget(left)
splitter1.addWidget(lineedit)
splitter1.setSizes([200, 200])
print(splitter1.handle(3))
splitter1.mousePressEvent = self.splitter_clicked
splitter2 = QSplitter(Qt.Vertical)
splitter2.setHandleWidth(self.splitter_handle_width)
splitter2.addWidget(splitter1)
splitter2.addWidget(bottom)
return splitter2
def splitter_clicked(self, event):
self.count += 1
print('splitter_double clicked' + str(self.count))
# def mousePressEvent(self, event):
# if event.button == Qt.RightButton:
# print('Right mouse clicked')
#
# elif event.button == Qt.LeftButton:
# print('Left mouse clicked')
def main():
splitter = QApplication([])
outer_layout = OuterLayout()
outer_layout.show()
splitter.exec_()
if __name__ == '__main__':
main()
The trick here is to create a custom QSplitterHandle class and override QSplitterHandle.mousePressEvent and a custom QSplitter class where you override createHandle so that it returns the custom QSplitterHandle instead of the standard one, i.e.
class MySplitter(QSplitter):
def createHandle(self):
return MySplitterHandle(self.orientation(), self)
class MySplitterHandle(QSplitterHandle):
def mousePressEvent(self, event):
if event.button() == Qt.RightButton:
print('Right mouse clicked')
elif event.button() == Qt.LeftButton:
print('Left mouse clicked')
super().mousePressEvent(event)
Finally, to use the custom QSplitter you need to replace all occurrences of QSplitter with MySplitter in SplitterLayout.

Differentiate between Button pressed and dragging in Qt?

According to the drag and drop documentation, I was able to implement the drag functionality on my QToolButton, but that is overriding standard button click behaviour and I am unable to check if the button was pressed or there was an intent to start a drag by dragging mouse.
Here is my QToolBar..
class toolbarButton(QToolButton):
def __init__(self, parent = None, item = None):
super(toolbarButton, self).__init__(parent)
self.setIcon(...)
self.setIconSize(QSize(40, 40))
self.dragStartPosition = 0
...
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragStartPosition = event.pos()
def mouseMoveEvent(self, event):
if not (event.buttons() and Qt.LeftButton):
return
if (event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance():
return
drag = QDrag(self)
mimeData = QMimeData()
...
drag.setMimeData(mimeData)
drag.exec(Qt.CopyAction)
def sizeHint(self):
return self.minimumSizeHint()
def minimumSizeHint(self):
return QSize(30, 30)
My initial thought, was to add the emit when the distance is less than startdragdistance but that would be incorrect as it would fire everytime I moved my mouse. Is there a way of achieving this in PyQt5? That I get standard QToolButton behaviour on button press and my custom behaviour on button drag?
When you override a method you are removing the default behavior, so that does not happen then you must call the parent method through super():
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() == Qt.LeftButton:
self.dragStartPosition = event.pos()

Signals or events for QMenu tear-off

Is there a signal/event I can make use of for the QMenu tear-off?
I have a QMenu subclass in which it has .setTearOffEnabled(True), but I would like to set this tear-off to always be on the top if the user clicks on the tear-off 'bar'.
I am unable to utilise QtCore.Qt.WindowStaysOnTopHint, as this will result in my menu already being in the tear-off state.
For example: if my main tool area is bigger than the tear-off, and I click on my main tool, the tear-off window will be behind it.
In the following code the clicked signal is emitted when the tear off (dotted lines) is pressed:
import sys
from PyQt5 import QtCore, QtWidgets
class Menu(QtWidgets.QMenu):
clicked = QtCore.pyqtSignal()
def mouseReleaseEvent(self, event):
if self.isTearOffEnabled():
tearRect = QtCore.QRect(
0,
0,
self.width(),
self.style().pixelMetric(
QtWidgets.QStyle.PM_MenuTearoffHeight, None, self
),
)
if tearRect.contains(event.pos()):
self.clicked.emit()
QtCore.QTimer.singleShot(0, self.after_clicked)
super(Menu, self).mouseReleaseEvent(event)
#QtCore.pyqtSlot()
def after_clicked(self):
tornPopup = None
for tl in QtWidgets.QApplication.topLevelWidgets():
if tl.metaObject().className() == "QTornOffMenu":
tornPopup = tl
break
if tornPopup is not None:
print("This is the tornPopup: ", tornPopup)
tornPopup.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow(parent=None)
menu = Menu("Menu", tearOffEnabled=True)
menu.clicked.connect(lambda: print("clicked"))
w.menuBar().addMenu(menu)
for i in range(5):
action = QtWidgets.QAction("action{}".format(i), w)
menu.addAction(action)
w.show()
sys.exit(app.exec_())
When a menu is torn off, it is hidden and Qt replaces it with a copy created from an internal subclass of QMenu. So to set the WindowStaysOnTopHint on a torn off menu, you would first need to find a way to get a reference to it. One way to do this would be to set an event-filter on the application object and watch for child events of the right type:
class MenuWatcher(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ChildAdded and
event.child().metaObject().className() == 'QTornOffMenu'):
event.child().setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)
This class will operate on all torn-off menus created in the application.
However, if the event-filtering was done by the source menu class, its own torn-off menu could be identified by comparing menu-items:
class Menu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setTearOffEnabled(True)
QtWidgets.qApp.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded:
child = event.child()
if (child.metaObject().className() == 'QTornOffMenu' and
all(a is b for a, b in zip(child.actions(), self.actions()))):
child.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint)
return super().eventFilter(source, event)

how get the QWidget from a not current tab in QtabBar in PyQt

lets say that i know/have the tabIndex from the tab below the cursor in the QTabBar, how could i get the widget inside of X tab in my tabBar? i dont Want the CurrentWidget(), because thats to easy, i want to get the widget of every tab that i want, in order to insert the right one into another QTabWidget when i drop it.
class ColtTab(qg.QTabWidget):
def __init__(self):
super(ColtTab,self).__init__()
self.setAcceptDrops(True)
self.tabBar = self.tabBar()
self.tabBar.setMouseTracking(True)
#self.tabBar.installEventFilter(self)
self.setDocumentMode(True)
self.indexTab = None
# Events
def mouseMoveEvent(self, e):
if e.buttons() != qc.Qt.MiddleButton:
return
globalPos = self.mapToGlobal(e.pos())
print(globalPos)
tabBar = self.tabBar
print(tabBar)
posInTab = tabBar.mapFromGlobal(globalPos)
print(posInTab)
self.indexTab = tabBar.tabAt(e.pos())
print(self.indexTab)
tabRect = tabBar.tabRect(self.indexTab)
print(tabRect)
print(tabRect.size())
pixmap = qg.QPixmap(tabRect.size())
tabBar.render(pixmap,qc.QPoint(),qg.QRegion(tabRect))
mimeData = qc.QMimeData()
drag = qg.QDrag(tabBar)
drag.setMimeData(mimeData)
drag.setPixmap(pixmap)
cursor = qg.QCursor(qc.Qt.OpenHandCursor)
drag.setHotSpot(e.pos() - posInTab)
drag.setDragCursor(cursor.pixmap(),qc.Qt.MoveAction)
dropAction = drag.exec_(qc.Qt.MoveAction)
def mousePressEvent(self, e):
#super().mousePressEvent(e)
if e.button() == qc.Qt.RightButton:
print('press')
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
e.setDropAction(qc.Qt.MoveAction)
e.accept()
self.insertTab(self.indexTab,self.currentWidget(), self.tabText(self.indexTab))
# HERE IN THE DROP EVENT I NEED TO INSERT THE CORRECT TAB INTO ANOTHER TABWIDGET, BUT I CAN ONLY INSERT THE CURRENT

Categories

Resources