QAction.setEnabled(False) unexpected behaviour - python

I have ascertained that making a QAction disabled does not in fact prevent code from being able to run activate() on it, which strikes me as curious. So I want to make a helper subclass:
class DeactivatableAction(QtWidgets.QAction):
def activate(self, event):
if self.isEnabled():
super().activate(event)
This seems to work in an app I'm working on, in practice. Then I wanted to include testing of this functionality (pytest):
#unittest.mock.patch('PyQt5.QtWidgets.QAction.activate')
def test_deactivatable_action_should_only_superactivate_if_enabled(mock_super):
import gen_fmwrk.deactivatable_action as d_action
QtWidgets.QApplication([]) # without this, I get a complaint about "Application not initialized"
da = d_action.DeactivatableAction()
da.setEnabled(False)
# da.setDisabled(True) - NB same effect as previous line
assert not da.isEnabled() # this fails!
da.activate(None)
assert not mock_super.called # this also fails
I realise this is a sort of disembodied way to run PyQt5 code... but I'd still expect to be able to disable a QAction in a pytest context like this. What's going wrong, and is there a solution?

The problem is that QtWidgets.QApplication([]) is not assigned to a variable so it will not be constructed correctly causing unexpected behavior. Change to
app = QtWidgets.QApplication([])

Related

Handling QDialog's events safely in PyQT

I'm having troubles with a Qt Application (managed through PyQt5) which used to be reliable, until a bunch of updates (where I ended up with PyQt6). PyQt6 is not the culprit here, as I began to have those problems with later PyQt5 versions.
I'm suspecting my problems are instead linked to an abuse of exec() methods call. The full traceback is not available (I've only a RuntimeError with a cryptic message telling me the Dialog is not available anymore); what (I think) I'm seeing are the parent windows randomly disappearing. This behaviour is indeed mentionned in the Qt documentation:
Note: Avoid using this function; instead, use open(). Unlike exec(), open() is asynchronous, and does not spin an additional event loop. This prevents a series of dangerous bugs from happening (e.g. deleting the dialog's parent while the dialog is open via exec()). When using open() you can connect to the finished() signal of QDialog to be notified when the dialog is closed.
Initially, my app was constructed this way:
QMainWindow A > QDialog B > QDialog C > ...
Each QDialog was launched using .exec() method.
The pseudo code could be summarized this way [edit] :
class A(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ui = Some_Ui_From_QtDesigner()
self.ui.setupUi(self)
self.ui.some_menu.triggered.connect(self.load_popup)
def load_popup():
B = QDialog(A)
if B.exec() == 1:
# Do some things (extract data from B and update A)
if C.exec() == 1:
# Do some things (extract data from C and update B)
qApp = QtWidgets.QApplication.instance()
a = A()
a.show()
qApp.setQuitOnLastWindowClosed(True)
qApp.exec()
I've tried to use .open() methods instead of .exec() and came up with something like this (using QLoopEvent's and events handling):
def process_C_qdialog(parent):
C = QDialog(parent)
# Construct your QDialog some more ...
loop = QtCore.QEventLoop()
my_result = None
def _accept():
my_result = "blah"
loop.exit(0)
def _reject():
loop.exit(-1)
f.accepted.connect(_accept)
f.rejected.connect(_reject)
f.open()
loop.exec()
return my_result
My troubles are far from over as yet. I suspect that recreating manually a QEventLoop causes the same problems than using .exec() in the first place.
So some questions:
Am I right about the QEventLoop?
Would it be safer/enough to leave the parent out of the QDialog generator?
Any advice on how to handle this the right way (keeping a modal dialog if possible)?
More generally, are they any safe ways to reproduce the static methods of Qt through a custom python function? (ie, a function which would load a customized popup, wait for the user's input and returning it)
Note: as this behaviour happens erratically (and without the full traceback available), I'm not 100% sure of this; but I suspect that even a simple QMessageBox().exec() has the same effects, even if this is not stated in the doc.

Trouble Understanding Signal Mapper PyQt

So I am generating a menu of options based on some files on my system. I have a list list of objects I need to dynamically generate an option in the menu for and need to be able to let the function that is doing the creation know which object to use. After some research I found the post below. I could not comment as my rep is not high yet: How to pass arguments to callback functions in PyQt
When I run this the signal mapper is not working right. It is not even calling the handleButton correctly. Any ideas as to how to use the signal mapper correctly?
from PyQt4 import QtGui, QtCore
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.mapper = QtCore.QSignalMapper(self)
self.toolbar = self.addToolBar('Foo')
self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
for text in 'One Two Three'.split():
action = QtGui.QAction(text, self)
self.mapper.setMapping(action, text)
action.triggered.connect(self.mapper.map)
self.toolbar.addAction(action)
self.mapper.mapped['QString'].connect(self.handleButton)
self.edit = QtGui.QLineEdit(self)
self.setCentralWidget(self.edit)
def handleButton(self, identifier):
print 'run'
if identifier == 'One':
text = 'Do This'
print 'Do One'
elif identifier == 'Two':
text = 'Do That'
print 'Do Two'
elif identifier == 'Three':
print 'Do Three'
text = 'Do Other'
self.edit.setText(text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(300, 60)
window.show()
sys.exit(app.exec_())
EDIT:
I've found that by using old-style signal/slot connections this is fixed:
#action.triggered.connect(self.mapper.map)
self.connect(action, QtCore.SIGNAL("triggered()"), self.mapper, QtCore.SLOT("map()"))
and
#self.mapper.mapped['QString'].connect(self.handleButton)
self.connect(self.mapper, QtCore.SIGNAL("mapped(const QString &)"), self.handleButton)
Am I using the new-style connections incorrectly?
Based on this post as well as the original link I posted, I thought I was doing things correctly.
The original example code (which I wrote), works perfectly fine for me using either Python2 or Python3 with several different recent versions of PyQt4. However, if I use a really old version of PyQt4 (4.7), the handler no longer gets called.
The reason (and solution) for this is given in the response to the mailing list post you linked to:
It's actually a problem with QSignalMapper.map() being called from a
proxy rather than new-style connections.
The workaround is to explicitly specify a signal that is compatible
with map()...
self.b1.clicked[()].connect(self.mapper.map)
Tonight's PyQt snapshot will be smarter about finding a usable Qt slot
before deciding that it needs to use a proxy so that the workaround
won't be necessary.
There are some signals (like clicked and triggered) which always send a default value unless you explicitly request otherwise. With the old-style signals, you can specify the no default overload it with SIGNAL("triggered()"), but with new-style signals, you have to do it like this:
action.triggered[()].connect(self.mapper.map)
But that is only necessary with very old versions of PyQt4 - the underlying issue was fixed back in 2010 (don't know the exact version, but 4.8 should be okay).

Overriding/Reimplementing Slots in PySide

I have almost the exact same question as the one found here:
Override shouldInterruptJavaScript in QWebPage with PySide
In my case though I want to override the copy and paste slots on QLineEdit
import sys
from PySide import QtGui, QtCore
class myLineEdit(QtGui.QLineEdit):
# FIXME: This is not working, the slot is not overriden!
#QtCore.Slot()
def copy(self):
print 'overridden copy event'
App.clipboard().setText('customized text')
return False
#QtCore.Slot()
def paste(self):
print 'overridden paste event'
self.setText('customized text')
return False
if __name__ == "__main__":
App = QtGui.QApplication(sys.argv)
Widget = myLineEdit()
Widget.show()
cmenu = Widget.createStandardContextMenu()
sys.exit(App.exec_())
I'm using Python 2.7.3, with PySide 1.2.2
I don't know if these methods are even supposed to be override-able, but I can't find any documentation that says they shouldn't be.
I also found this page
http://qt-project.org/faq/answer/is_it_possible_to_reimplement_non-virtual_slots
The page explains how certain kinds of slots get pointers set to them by functions that get called when the object is initialized (or something along those lines, I'm not as familiar with the C++).
Following this logic I added the createStandardContextMenu() call above in the hopes that it would reinitialize the slots for at least the context menu, but no luck.
Am I doing something wrong? Or should I try filing a bug report?
You cannot override QLineEdit.copy or QLineEdit.paste in such a way that they will be called internally by Qt.
In general, you can only usefully override or reimplement Qt functions that are defined as being virtual. The Qt Docs will always specify whether this is the case, and for QLineEdit, there are no public slots that are defined in that way.
There is also no easy workaround. There are a lot of different ways in which copy and paste operations (or their equivalents) can be invoked, such as keyboard shortcuts, context menu, drag and drop, etc. It can be done: but it's a lot of work to get complete control over all these sorts of operations. So you need to think carefully about what you're trying to achieve before deciding how to proceed.

Calling a class method in Python/PySide

I spent longer than I'd care to admit think of a suitable 'question' heading for this topic, as my issue is somewhat hard to articulate.
Here is a quick summary of the situation:
I'm writing a basic GUI with Python 3.4 and PySide
I'm using QFileSystemWatcher to monitor a particular file
When the file is changed, QFileSystemWatcher calls a method, which in turn calls a method within a PySide Class
All of the above seems to be working perfectly, except the GUI-specific actions detailed in the PySide Class method aren't being executed (I'll explain in more detail below).
Example code:
#Establishing the PySide GUI Class
class GUI(QMainWindow, Ui_GUI):
def __init__(self, parent=None)
super(GUI, self).__init__(parent)
self.setupUi(self)
QtCore.QObject.connect(self.Button, QtCore.SIGNAL("clicked()"), self.Run)
def Run(self):
print("1")
self.treeWidget1.clear()
self.treeWidget2.clear()
print("2")
self.label1.setText("Text 1")
self.label2.setText("Text 2")
print("3")
for y in range(0, 5):
self.treeWidget1.resizeColumnsToContents()
print("Finished")
#Establish the file monitoring mechanism, *outside* the PySide class
def FileChanged():
Script = GUI()
Script.Run()
Paths = ['path/to/file']
Watch = QtCore.QFileSystemWatcher(Paths)
Watch.fileChanged.connect(FileChanged)
#Setting up the GUI
if __name__ == '__main__':
app = QApplication(sys.argv)
showGUI = GUI()
showGUI.show()
app.exec_()
As I mentioned above, the above code doesn't return any errors. When I change the file (listed in the path), FileChanged does indeed call the Run() method from the GUI class. However, it won't actually do any of the 'stuff', it will only execute the print commands in between the 'stuff'.
If I then click on the 'Button' in the GUI, it will execute Run() correctly, and properly execute all the 'stuff'.
My question: is there something I'm missing here? If it's calling the method correctly, and is able to execute the various 'print' commands, why is it not executing the actual 'stuff'?
Thanks!
EDIT 1: I've removed the -do stuff- tags and put in some example code. All the 'stuff' code relates to updating various PySide QLabels, QTreeWidgets, etc.
EDIT 2: I forget the () at the end of the treeWidget clear commands.
The Script object created in the FileChanged function has local scope, and will be garbage-collected as soon as the function returns.
If the Run slot gets called when the signal fires, it will carry out all of the changes correctly, but you won't get to see any of those changes, because Script will be deleted before it is ever shown.
In order to for the example script to begin to make any sense, it would need to be re-arranged to something like this:
#Setting up the GUI
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
showGUI = GUI()
#Establish the file monitoring mechanism, *outside* the PySide class
def FileChanged():
showGUI.Run()
Paths = ['path/to/file']
Watch = QtCore.QFileSystemWatcher(Paths)
Watch.fileChanged.connect(FileChanged)
showGUI.show()
app.exec_()
Of course, it's possible that your real code is nothing like the example in your question (which has numerous other errors preventing it from being run), and so this might be no help. But if that is the case, you will have to post a fully working, self-contained example that properly demonstrates the problems you are having.

Enter-Notify-Event Signal not working on gtk.ToolButton

On a happy (if not irrevelent) note, this is the absolute last obstacle in this particular project. If I fix this, I have my first significant dot release (1.0), and the project will be going public. Thanks to everyone here on SO for helping me through this project, and my other two (the answers help across the board, as they should).
Now, to the actual question...
I have a toolbar in my application (Python 2.7, PyGTK) which has a number of gtk.ToolButton objects on it. These function just fine. I have working "clicked" events tied to them.
However, I need to also connect them to "enter-notify-event" and "leave-notify-event" signals, so I can display the button's functions in the statusbar.
This is the code I have. I am receiving no errors, and yet, the status bar messages are not appearing:
new_tb = gtk.ToolButton(gtk.STOCK_NEW)
toolbar.insert(new_tb, -1)
new_tb.show()
new_tb.connect("clicked", new_event)
new_tb.connect("enter-notify-event", status_push, "Create a new, empty project.")
new_tb.connect("leave-notify-event", status_pop)
I know the issue is not with the "status_push" and "status_pop" events, as I've connected all my gtk.MenuItem objects to them, and they work swimmingly.
I know that gtk.ToolButton objects are in the Widgets class, so "enter-notify-event" and "leave-notify-event" SHOULD technically work. My only guess is that this particular object does not emit any signals other than "clicked", and thus I'd have to put each in a gtk.EventBox.
What am I doing wrong here? How do I fix this?
Thanks in advance!
Your guess was correct, you should wrap your widget in a gtk.EventBox, here is an example that i hope will be hopeful:
import gtk
def callback(widget, event, data):
print event, data
class Win(gtk.Window):
def __init__(self):
super(Win, self).__init__()
self.connect("destroy", gtk.main_quit)
self.set_position(gtk.WIN_POS_CENTER)
self.set_default_size(250, 200)
tb = gtk.ToolButton(gtk.STOCK_NEW)
# Wrap ``gtk.ToolButton`` in an ``gtk.EventBox``.
ev_box = gtk.EventBox()
ev_box.connect("enter-notify-event", callback, "enter")
ev_box.connect("leave-notify-event", callback, "leave")
ev_box.add(tb)
self.add(ev_box)
if __name__ == '__main__':
Win()
gtk.main()
It appears, based on experimentation and evidence, this is impossible in PyGtk 2.24.

Categories