copy/select text in WhatsThis - python

Example:
class tab_1(QWidget):
def __init__(self):
super(tab_1, self).__init__()
self.initUI()
def initUI(self):
self.btn = QPushButton(self)
self.btn.setText("tab1")
self.btn.setWhatsThis(
"""''(parameter) self: ~tab
Sourcery Code Metrics
Complexity 0 ⭐
Size 108 🙂
Working Memory 4 ⭐
Quality Score 82 % ⭐
⟠ self: [tab] Docc"""
""
)
I use the setWhatsThis and try to copy/select the text of it, but
when I click the mouse
In WhatsThis area is Disappear!
Example

In general, the what's this behavior is to only handles clicks on hyperlinks (which normally launch the application help dialog), so if the user clicks it, it will automatically close anyway.
The only solution is to override the default behavior by mimicking what Qt normally does: install an event filter on the application whenever a top level window receives a EnterWhatsThisMode event type, and create a custom widget that is shown whenever it's appropriate.
Since this will cause to override all what's this cases, I'd suggest to also set the WA_CustomWhatsThis attribute for the widgets that will have selectable text, so that you can use the standard behavior for any other case.
The custom widget is actually a QLabel with the Popup window flag set, which will make the widget a top level window that also captures mouse events, and with the TextSelectableByMouse and LinksAccessibleByMouse text interaction flags set.
class SelectableWhatsThis(QtWidgets.QLabel):
def __init__(self, parent, pos):
super().__init__(parent.whatsThis(), parent, flags=QtCore.Qt.Popup)
self.setTextInteractionFlags(
QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse)
self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
self.setForegroundRole(QtGui.QPalette.ToolTipText)
self.setAutoFillBackground(True)
self.setContentsMargins(12, 8, 12, 8)
self.move(pos)
self.show()
def mousePressEvent(self, event):
if event.pos() not in self.rect():
self.close()
else:
super().mousePressEvent(event)
def keyPressEvent(self, event):
self.close()
class WhatsThisHelper(QtCore.QObject):
installed = active = False
whatsThis = None
def __init__(self, parent):
super().__init__(parent)
if not self.installed:
# ensure that only one instance of WhatsThisHelper is installed
self.__class__.installed = True
QtWidgets.QApplication.instance().installEventFilter(self)
self.active = True
def eventFilter(self, obj, event):
if not obj.isWidgetType():
return False
if (event.type() == event.MouseButtonPress
and event.button() == QtCore.Qt.LeftButton
and obj.whatsThis() and
obj.testAttribute(QtCore.Qt.WA_CustomWhatsThis)):
self.whatsThis = SelectableWhatsThis(obj, event.globalPos())
QtWidgets.QApplication.restoreOverrideCursor()
return True
elif event.type() in (event.MouseButtonRelease, event.MouseButtonDblClick):
if self.whatsThis:
QtWidgets.QWhatsThis.leaveWhatsThisMode()
return True
elif event.type() == event.LeaveWhatsThisMode:
QtWidgets.QApplication.instance().removeEventFilter(self)
self.deleteLater()
return super().eventFilter(obj, event)
def __del__(self):
if self.active:
self.__class__.installed = False
class MainWindow(QtWidgets.QMainWindow):
# ...
def event(self, event):
if event.type() == event.EnterWhatsThisMode:
WhatsThisHelper(self)
return super().event(event)
Note that you need to override the event of all widgets that are going to be top level widgets (all QMainWindows, QDialogs, etc) and need to support this.

Related

Drawing Line from QLabel to QLabel in PyQt

I'm fairly new to PyQt
I'm trying to drawing a line from 1 QLabel to another.
My 2 QLabel are located on another QLabel which acts as an image in my GUI.
I've managed to track the mouse event and move the label around, but I cannot draw the line between them using QPainter.
Thank you in advance :)
This is my MouseTracking class
class MouseTracker(QtCore.QObject):
positionChanged = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.setMouseTracking(True)
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, o, e):
if e.type() == QtCore.QEvent.MouseMove:
self.positionChanged.emit(e.pos())
return super().eventFilter(o, e)
This is my DraggableLabel class:
class DraggableLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.LabelIsMoving = False
self.setStyleSheet("border-color: rgb(238, 0, 0); border-width : 2.0px; border-style:inset; background: transparent;")
self.origin = None
# self.setDragEnabled(True)
def mousePressEvent(self, event):
if not self.origin:
# update the origin point, we'll need that later
self.origin = self.pos()
if event.button() == Qt.LeftButton:
self.LabelIsMoving = True
self.mousePos = event.pos()
# print(event.pos())
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
# move the box
self.move(self.pos() + event.pos() - self.mousePos)
# print(event.pos())
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
print(event.pos())
def paintEvent(self, event):
painter = QPainter()
painter.setBrush(Qt.red)
# painter.setPen(qRgb(200,0,0))
painter.drawLine(10, 10, 200, 200)
This is my custom class for the QTabwigdet (since I need to control and track the position of 2 QLabels whenever the user add/insert a new Tab)
class DynamicTab(QWidget):
def __init__(self):
super(DynamicTab, self).__init__()
# self.count = 0
self.setMouseTracking(True)
self.setAcceptDrops(True)
self.bool = True
self.layout = QVBoxLayout(self)
self.label = QLabel()
self.layout.addChildWidget(self.label)
self.icon1 = DraggableLabel(parent=self)
#pixmap for icon 1
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon1.setPixmap(pixmap)
self.icon1.setScaledContents(True)
self.icon1.setFixedSize(20, 20)
self.icon2 = DraggableLabel(parent=self)
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon2.setPixmap(pixmap)
self.icon2.setScaledContents(True)
self.icon2.setFixedSize(20, 20)
#self.label.move(event.x() - self.label_pos.x(), event.y() - self.label_pos.y())
MainWindow and main method:
class UI_MainWindow(QMainWindow):
def __init__(self):
super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
self.PictureTab = QTabWidget
def __setupUI__(self):
# super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
loadUi("IIML_test2.ui", self)
self.tabChanged(self.PictureTab)
# self.tabChanged(self.tabWidget)
self.changeTabText(self.PictureTab, index=0, TabText="Patient1")
self.Button_ImportNew.clicked.connect(lambda: self.insertTab(self.PictureTab))
# self.PictureTab.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.PictureTab))
# self.tabWidget.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.tabWidget))
def tabChanged(self, QtabWidget):
QtabWidget.currentChanged.connect(lambda : print("Tab was changed to ", QtabWidget.currentIndex()))
def changeTabText(self, QTabWidget, index, TabText):
QTabWidget.setTabText(index, TabText)
def insertTab(self, QtabWidget):
# QFileDialog.getOpenFileNames(self, 'Open File', '.')
QtabWidget.addTab(DynamicTab(), "New Tab")
# get number of active tab
count = QtabWidget.count()
# change the view to the last added tab
currentTab = QtabWidget.widget(count-1)
QtabWidget.setCurrentWidget(currentTab)
pixmap = QPixmap('cat.jpg')
#currentTab.setLayout(QVBoxLayout())
#currentTab.layout.setWidget(QRadioButton())
# currentTab.setImage("cat.jpg")
currentTab.label.setPixmap(pixmap)
currentTab.label.setScaledContents(True)
currentTab.label.setFixedSize(self.label.width(), self.label.height())
tracker = MouseTracker(currentTab.label)
tracker.positionChanged.connect(self.on_positionChanged)
self.label_position = QtWidgets.QLabel(currentTab.label, alignment=QtCore.Qt.AlignCenter)
self.label_position.setStyleSheet('background-color: white; border: 1px solid black')
currentTab.label.show()
# print(currentTab.label)
#QtCore.pyqtSlot(QtCore.QPoint)
def on_positionChanged(self, pos):
delta = QtCore.QPoint(30, -15)
self.label_position.show()
self.label_position.move(pos + delta)
self.label_position.setText("(%d, %d)" % (pos.x(), pos.y()))
self.label_position.adjustSize()
# def SetupUI(self, MainWindow):
#
# self.setLayout(self.MainLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
UI_MainWindow = UI_MainWindow()
UI_MainWindow.__setupUI__()
widget = QtWidgets.QStackedWidget()
widget.addWidget(UI_MainWindow)
widget.setFixedHeight(900)
widget.setFixedWidth(1173)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
My concept: I have a DynamicTab (QTabWidget) which acts as a picture opener (whenever the user press Import Now). The child of this Widget are 3 Qlabels: self.label is the picture it self and two other Qlabels are the icon1 and icon2 which I'm trying to interact/drag with (Draggable Label)
My Problem: I'm trying to track my mouse movement and custom the painter to paint accordingly. I'm trying that out by telling the painter class to paint whenever I grab the label and move it with my mouse (Hence, draggable). However, I can only track the mouse position inside the main QLabel (the main picture) whenever I'm not holding or clicking my left mouse.
Any help will be appreciated here.
Thank you guys.
Painting can only happen within the widget rectangle, so you cannot draw outside the boundaries of DraggableLabel.
The solution is to create a further custom widget that shares the same parent, and then draw the line that connects the center of the other two.
In the following example I install an event filter on the two draggable labels which will update the size of the custom widget based on them (so that its geometry will always include those two geometries) and call self.update() which schedules a repainting. Note that since the widget is created above the other two, it might capture mouse events that are intended for the others; to prevent that, the Qt.WA_TransparentForMouseEvents attribute must be set.
class Line(QWidget):
def __init__(self, obj1, obj2, parent):
super().__init__(parent)
self.obj1 = obj1
self.obj2 = obj2
self.obj1.installEventFilter(self)
self.obj2.installEventFilter(self)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
def eventFilter(self, obj, event):
if event.type() in (event.Move, event.Resize):
rect = self.obj1.geometry() | self.obj2.geometry()
corner = rect.bottomRight()
self.resize(corner.x(), corner.y())
self.update()
return super().eventFilter(obj, event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(painter.Antialiasing)
painter.setPen(QColor(200, 0, 0))
painter.drawLine(
self.obj1.geometry().center(),
self.obj2.geometry().center()
)
class DynamicTab(QWidget):
def __init__(self):
# ...
self.line = Line(self.icon1, self.icon2, self)
Notes:
to simplify things, I only use resize() (not setGeometry()), in this way the widget will always be placed on the top left corner of the parent and we can directly get the other widget's coordinates without any conversion;
the custom widget is placed above the other two because it is added after them; if you want to place it under them, use self.line.lower();
the painter must always be initialized with the paint device argument, either by using QPainter(obj) or painter.begin(obj), otherwise no painting will happen (and you'll get lots of errors in the output);
do not use layout.addChildWidget() (which is used internally by the layout), but the proper addWidget() function of the layout;
the stylesheet border syntax can be shortened with border: 2px inset rgb(238, 0, 0);;
the first lines of insertTab could be simpler: currentTab = DynamicTab() QtabWidget.addTab(currentTab, "New Tab");
currentTab.label.setFixedSize(self.label.size());
QMainWindow is generally intended as a top level widget, it's normally discouraged to add it to a QStackedWidget; note that if you did that because of a Youtube tutorial, that tutorial is known for suggesting terrible practices (like the final try/except block) which should not be followed;
only classes and constants should have capitalized names, not variables and functions which should always start with a lowercase letter;

Widget dragging not working when hovering over the widget itself

I'm having an issue where I have implemented a dragging feature for widgets using eventFilter(), but it seems when I drag towards the right and my cursor hovers over the widget which in my case is a QPushButton it seems to stop tracking until I hover out of the widget
How would I fix this?
class widget1(QWidget):
def __init__(self):
super().__init__()
self.widget = QPushButton("button0", self)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.createbutton = QPushButton('+', self)
self.createbutton.setGeometry(5, 5, 15, 15)
self.createbutton.clicked.connect(self.createWidget)
self.show()
def createWidget(self):
self.new_widget = widget1()
self.new_widget.setParent(self)
self.new_widget.show()
self.new_widget.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QEvent.MouseMove:
MousePos = QPoint(event.pos())
if event.buttons() == Qt.LeftButton:
source.move(source.x() + MousePos.x(), source.y() + MousePos.y())
return super(MainWindow, self).eventFilter(source, event)
Basically I have a class with a widget in it called widget1() (This will be my custom widget in the future) I am then adding it dynamically to the window every time I press self.createbutton using .show() instead of using a layout, then it installs the event filter to it so that it can allow for dragging.
The event filter is receiving events for the widget1 instance but not for its child widgets (i.e. the QPushButton). A quick fix is to invoke QMouseEvent.ignore for the QPushButton's mouseMoveEvent so the mouse event will be propagated up to the parent widget and received in the event filter.
class widget1(QWidget):
def __init__(self):
super().__init__()
self.widget = QPushButton("button0", self)
self.widget.mouseMoveEvent = lambda event: event.ignore()

Qt5 mouseEntered / mouseExited signal?

Is there a mouseEntered / mouseExited signal in Qt? (Equivalent to mouseEntered / mouseExited from Java) I can't seem to find it.
You will have to subclass your widget and install an eventFilter onto it.
class MyButton(QPushButton):
def __init__(self):
super(MyButton, self).__init__()
self.show()
self.installEventFilter(self) //Install event filter on widget
def eventFilter(self, object, event): //Define eventFilter method
if event.type() == Event.Enter:
print(“Mouse is hovering over button”)
elif event.type() == QEvent.Leave:
print(“Mouse has left the button”)
return False

How to ignore the first mouse event in the QToolButton Menu to prevent unwanted selection of items

A colleague made a custom QMenu derivative so that one can select multiple entries before the menu closes.
It is triggered via a QToolButton.
The problem is if the menu is large enough it will overlap with the button. The item at the current cursor position then gets selected instantly when clicking the QToolButton.
How does one prevent this?
Code for my menu, I tried to ignore the first event with a Bool flag, but it doesn't work.
class StayOpenMenu(QMenu):
"""
a class that overrides the QMenu mouseReleaseEvent to let the menu stay open when an element is selected
"""
def __init__(self, parent=None):
self.isfirstEvent = True
super().__init__("Stay open Menu", parent=parent)
def mouseReleaseEvent(self, a0: QMouseEvent):
if self.isfirstEvent:
a0.ignore()
self.isfirstEvent = False
return
try:
action = self.actionAt(a0.pos())
action.trigger()
except:
pass
def aboutToShow(self):
self.isfirstEvent = True
return super().aboutToShow()
def aboutToHide(self):
self.isfirstEvent = True
return super().aboutToShow()
Image: Before clicking the button
Image: After clicking the QToolButton
aboutToShow() and aboutToHide() are signals, not methods, so they can't be "overridden".
Create a slot for setting the variable to True and connect it to the aboutToShow signal only.
Also note that you'll have to take care of the mousePressEvent too: if the menu is not activated from a mouse click (most likely by pressing the tool button via keyboard), it will prevent it from receiving a legitimate release event.
class StayOpenMenu(QMenu):
def __init__(self, parent=None):
self.isfirstEvent = False
super().__init__("Stay open Menu", parent=parent)
self.aboutToShow.connect(self.isShowing)
def isShowing(self):
self.isfirstEvent = True
def mousePressEvent(self, a0: QMouseEvent):
self.isfirstEvent = False
super().mousePressEvent(a0)
def mouseReleaseEvent(self, a0: QMouseEvent):
if self.isfirstEvent:
a0.ignore()
self.isfirstEvent = False
return
try:
action = self.actionAt(a0.pos())
action.trigger()
except:
pass

How can I know when the Enter key was pressed on QTextEdit

I'm writing Chat gui for client on Python using PyQt5.
I have a QTextEdit, which the client can write messages in it.
I wan't to know when the 'Enter' key is being pressed while the focus is on the QTextEdit.
I tried using installEventFilter function but it detects keys being pressed on all of the other widgets but the QTextEdit one.
What can I do to fix that?
def initUI(self):
# ...
self.text_box = QtWidgets.QTextEdit(self)
self.installEventFilter(self)
# ...
def keyPressEvent(self, qKeyEvent):
print(qKeyEvent.key())
if qKeyEvent.key() == Qt.Key_Return:
if self.text_box.hasFocus():
print('Enter pressed')
When you override keyPressEvent you are listening to the events of the window, instead install an eventFilter to the QTextEdit, not to the window as you have done in your code, and check if the object passed as an argument is the QTextEdit:
def initUI(self):
# ...
self.text_box = QtWidgets.QTextEdit(self)
self.text_box.installEventFilter(self)
# ...
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.KeyPress and obj is self.text_box:
if event.key() == QtCore.Qt.Key_Return and self.text_box.hasFocus():
print('Enter pressed')
return super().eventFilter(obj, event)
The answer from #eyllanesc is very good if you are determined to use QTextEdit.
If you can get away with QLineEdit and its limitations, you can use the returnPressed() signal. The biggest drawback for QLineEdit is you are limited to one line of text. And there is no word wrap. But the advantage is you don't have to mess with eventFilters or think too hard about how keyPress signals fall through all of the widgets in your app.
Here is a minimal example that copies from one QLineEdit to another:
import sys
from PyQt5.QtWidgets import *
class PrintWindow(QMainWindow):
def __init__(self):
super().__init__()
self.left=50
self.top=50
self.width=300
self.height=300
self.initUI()
def initUI(self):
self.setGeometry(self.left,self.top,self.width,self.height)
self.line_edit1 = QLineEdit(self)
self.line_edit1.move(50, 50)
self.line_edit1.returnPressed.connect(self.on_line_edit1_returnPressed)
self.line_edit2 = QLineEdit(self)
self.line_edit2.move(50, 100)
self.show()
def on_line_edit1_returnPressed(self):
self.line_edit2.setText(self.line_edit1.text())
if __name__ == '__main__':
app = QApplication(sys.argv)
window = PrintWindow()
sys.exit(app.exec_())
In this example, I have manually connected to the signal in line 22 (self.line_edit1.returnPressed.connect). If you are using a ui file, this connection can be left out and your program will automatically call the on__returnPressed method.
When you override keyPressEvent you are listening to the events of the window, instead install an eventFilter to the QTextEdit, not to the window as you have done in your code, and check if the object passed as an argument is the QTextEdit:
def initUI(self):
# ...
self.text_box = QtWidgets.QTextEdit(self)
self.text_box.installEventFilter(self)
# ...
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.KeyPress and obj is self.text_box:
if event.key() == QtCore.Qt.Key_Return and self.text_box.hasFocus():
print('Enter pressed')
return True
return False
This is building upon the answer of #eyllanesc and the problem #Daniel Segal faced. Adding the correct return values as such to the eventFilter solves the problem.

Categories