GTK: Problems redrawing widgets on Gtk.ApplicationWindow - python

I'm creating a GTK GUI in Python for an already existing program. Imagine the following structure:
program
|----gtk
|-----application.py
|-----mainwindow.py
|-----mainwidgets.py
Of course this is simplified. But the GUI works somewhat like this: application.py is a Gtk.Application that creates instances of the objects in mainwidgets, provides back-end functions which are passed as callbacks to mainwindow.py, which places the widgets in a Gtk.ApplicationWindow.
There are a couple of cases where I'm having trouble. I'll go over two which I think are related.
Let's go for the simpler one first. There's a simple button defined in mainwidget, which is used as a statusbar in my program.
class Statusbar(Gtk.Widget):
def __init__(self, on_button_do):
super(Gtk.Widget, self).__init__()
self.callback = on_button_do
self.button_label_int = 0
self.button = Gtk.Button.new_with_label(str(button_label_int)
def increment_button_label(self):
self.button_label_int += 1
self.button.set_label(str(button_label))
Then, when the mainwindow receives a signal, it is instructed to call
statusbar.increment_button_label(). This crashes almost immediately with a segmentation fault, no more info given about the problem, as soon as that signal is received.
class AppWindow(Gtk.ApplicationWindow):
__gsignals__ = {
"new_log": (GObject.SIGNAL_RUN_FIRST, None, ())
}
def __init__(self, statusbar, sidebar, *args, **kwargs):
super(Gtk.ApplicationWindow, self).__init__(*args, **kwargs)
self.statusbar = statusbar
self.sidebar = sidebar
self.notificationBox = Gtk.Box()
self.notificationBox.pack_start(self.statusbar.get_button(), True, True, 0)
# MAINBOX: THE BIGGER BOX OF ALL THE LITTLE BOXES
self.mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.mainBox.pack_start(self.
self.mainBox.pack_start(self.notificationBox, False, False, 0)
self.add(self.mainBox)
self.show_all()
def do_new_log(self):
"""What should the window do when it gets a new_log signal"""
statusbar.increment_button_label()
Another (related?) problem having to with updating the display of widgets: my sidebar. The sidebar is actually just a Gtk.ListStore and a Gtk.TreeView, nothing fancy. In application.py you'd find code like this:
def do_startup(self):
Gtk.Application.do_startup(self) # deep GTK magic
self.sidebar = Sidebar(self.workspace_manager,
self.changeWorkspace,
CONF.getLastWorkspace())
And then that instance of sidebar is passed to mainwindow.py to be put in a box. Everything works fine for now.
The problem comes when I try to add information to the sidebar. The back-end part of it works, because if I restart the application, I can see the new entry in the sidebar. And the sidebar gets information from this backend:
class Sidebar(Gtk.Widget):
def __init__(self, workspace_manager, callback_to_change_workspace, conf):
super(Gtk.Widget, self).__init__()
self.callback = callback_to_change_workspace
self.ws_manager = workspace_manager
self.lastWorkspace = conf
def createTitle(self):
title = Gtk.Label()
title.set_text("Workspaces")
return title
def workspaceModel(self):
self.workspace_list_info = Gtk.ListStore(str)
for ws in self.ws_manager.getWorkspacesNames():
treeIter = workspace_list_info.append([ws])
if ws == self.lastWorkspace:
self.defaultSelection = treeIter
def workspaceView(self):
self.lst = Gtk.TreeView(self.workspace_list_info)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Workspaces", renderer, text=0)
self.lst.append_column(column)
# select by default the last active workspace
if self.defaultSelection is not None:
self.selectDefault = self.lst.get_selection()
self.selectDefault.select_iter(self.defaultSelection)
self.selection = self.lst.get_selection()
self.selection.connect("changed", self.callback)
What maindindow tries to do is put sidebar.lst into a box. That works fine. The thing is when I add a workspace via a dialog box, I doesn't show up, as I stated above.
Any idea about what could cause these problems? Is this way of organizing my problem not all right for some reason for GTK? I think the code itself is fine: the workspaces are added, after all, and GTK doesn't crash. It just doesn't do it fine. Also the button, at first, is displayed just fine, it even emits its signal. But as soon as I try to change its label, everything explodes.

OK, so this actually had to do with multiple threads and signals. It was solved by overriding the emit signal.
class _IdleObject(GObject.GObject):
"""
Override GObject.GObject to always emit signals in the main thread
by emmitting on an idle handler
"""
def __init__(self):
GObject.GObject.__init__(self)
def emit(self, *args):
GObject.idle_add(GObject.GObject.emit, self, *args)

Related

Menubar sometimes does not become un-greyed when QFileDialog closes

OS: W10. This may be significant. If you have different results on a different platform, feedback would be helpful.
Here is an MRE. If you run it and go Ctrl+O, the menu labels become greyed. If you select a file in the QFileDialog by clicking the "Open" button or using its mnemonic (Alt+O), the open-file dialog is dismissed and the "Files" and "Help" menus become un-greyed.
However, if you go Ctrl+O again, and this time enter the name of a file in the "File name" box (QLineEdit), and then press Return, the dialog is dismissed (with a successful selection result) but the "Files" and "Help" menus remain greyed-out. It looks like this:
import sys, os
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('Greying of menus MRE')
self.setGeometry(QtCore.QRect(100, 100, 400, 200))
menubar = QtWidgets.QMenuBar(self)
self.setMenuBar(menubar)
self.files_menu = QtWidgets.QMenu('&Files', self)
menubar.addMenu(self.files_menu)
self.help_menu = QtWidgets.QMenu('&Help', self)
menubar.addMenu(self.help_menu)
self.new_action = QtWidgets.QAction('&New', self)
self.files_menu.addAction(self.new_action)
self.open_action = QtWidgets.QAction('&Open', self)
self.files_menu.addAction(self.open_action)
self.open_action.setShortcut("Ctrl+O")
self.open_action.triggered.connect(self.open_file)
def focusInEvent(self, event ):
print('main_window focusInEvent')
super().focusInEvent(event)
def focusOutEvent(self, event ):
print('main_window focusOutEvent')
super().focusInEvent(event)
def activateWindow(self):
print('main_window activateWindow')
super().activateWindow()
def open_file(self):
print('open file')
main_window_self = self
# open_doc_dialog = QtWidgets.QFileDialog(self.get_main_window())
class OpenDocFileDialog(QtWidgets.QFileDialog):
def accepted(self):
print('accepted')
super().accepted()
def accept(self):
print('accept')
super().accept()
def close(self):
print('close')
super().close()
def done(self, r):
print(f'done r {r}')
# neither of these solves the problem:
# main_window_self.activateWindow()
# main_window_self.files_menu.activateWindow()
super().done(r)
def hide(self):
print(f'hide')
super().hide()
def focusInEvent(self, event ):
print('focusInEvent')
super().focusInEvent(event)
def focusOutEvent(self, event ):
print('focusOutEvent')
super().focusInEvent(event)
def activateWindow(self):
print('activateWindow')
super().activateWindow()
open_doc_dialog = OpenDocFileDialog(self)
open_doc_dialog.setWindowTitle('Choose file')
open_doc_dialog.setDirectory(os.getcwd())
# we cannot use the native dialog, because we need control over the UI
options = open_doc_dialog.Options(open_doc_dialog.DontUseNativeDialog)
open_doc_dialog.setOptions(options)
open_doc_button = open_doc_dialog.findChild(QtWidgets.QDialogButtonBox).button(QtWidgets.QDialogButtonBox.Open)
lineEdit = open_doc_dialog.findChild(QtWidgets.QLineEdit)
# this does not solve the problem
# lineEdit.returnPressed.disconnect()
# lineEdit.returnPressed.connect(open_doc_button.click)
print(f'open_doc_button {open_doc_button}, lineEdit {lineEdit}')
# show the dialog
dialog_code = open_doc_dialog.exec()
if dialog_code != QtWidgets.QDialog.Accepted: return
sel_files = open_doc_dialog.selectedFiles()
print(f'sel_files: {sel_files}')
app = QtWidgets.QApplication([])
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())
This problem can be understood, if not solved, with reference to this answer.
Note that this greying-out is not disablement. As explained in the above link, this has to do with "active/inactive states" of the menus (or their labels). The menus remain enabled throughout, although in this case it's impossible to know that while the open-file dialog is showing because it is modal. Clicking on one menu after the dialog has gone, or just hovering over it, is enough to un-grey them both...
The explanation, as I understand it, is that the "File name" box QLineEdit has a signal, returnPressed, which appears to activate something subtley different to the slot which is invoked when you use the "Choose" button. You can see I have experimented with trying to re-wire that signal, to no avail.
The method done of the QFileDialog appears to be called however the dialog closes (unlike close!), so I tried "activating" the main window... and then the individual QMenus... Doesn't work.
I am not clear how to get a handle on this "active state" business or why the slot connected to returnPressed is (seemingly) unable to give the "active state" back to the menus when the other slot manages to do so.
Edit
Searching on Musicamante's "unpolishing" suggestion led me to this:
lineEdit.returnPressed.disconnect()
def return_pressed():
style = main_window_self.menubar.style()
style.unpolish(main_window_self.menubar)
open_doc_button.click()
lineEdit.returnPressed.connect(return_pressed)
... unfortunately this doesn't work.
This looks like a possible Windows-related bug, since I can't reproduce it on Linux. As a work-around, you could try forcing a repaint after the dialog closes:
# show the dialog
dialog_code = open_doc_dialog.exec()
self.menubar.repaint()
Finally got it, thanks to Musicamante's suggestion:
lineEdit.returnPressed.disconnect()
def return_pressed():
style = main_window_self.menubar.style()
style.unpolish(main_window_self.menubar)
open_doc_button.click()
main_window_self.menubar.repaint()
lineEdit.returnPressed.connect(return_pressed)
... I actually tried this several times, just to make sure it was doing what was intended. So in fact, fortunately, no single-shot timer was needed in this case.

refresh(update) widget in Nuke (vfx soft)

I need refresh(update) GUI widget(pySide) in Nuke(compositing software) after load or save nuke script.
callback: nuke.addOnScriptSave() and nuke.addOnScriptLoad()
import nuke
from PySide import QtGui, QtCore
from nukescripts import panels
class Info(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.initUI()
def scriptName(self):
sName = (nuke.root().name()).split('/')#split name from root
return sName
def initUI(self):
self.lbl1 = QtGui.QLabel("script name : " , self)
layout = QtGui.QHBoxLayout()#main layout
layout.addWidget(self.lbl1)
self.setLayout(layout)
self.updateInfo()
def updateInfo(self):
scriptName = self.scriptName()
self.lbl1.setText("script name : " + scriptName[-1].split('.')[0])#set name
panels.registerWidgetAsPanel('Info', 'Info_script', 'infoscript')
The simple solution is to add the callback registration to your widget's __init__:
class Info(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.initUI()
nuke.addOnScriptSave(self.updateInfo)
if nuke.root().name() == 'Root' and not nuke.modified():
# No reason to add a scriptLoad callback if opening a
# script would spawn a new Nuke process.
nuke.addOnScriptLoad(self.updateInfo)
# Rest of the class definition omitted for brevity
This approach does have a notable downside: Because Nuke's callback registry will now contain at least one reference to the widget's updateInfo method, the widget can never be garbage-collected by Python. This will probably never be too big of a deal in real life, since you will probably only ever create a very small number of panel instances, but if you ever created, say, 1000 instances, you would have 1000 callbacks registered that would never be unregistered (even if the panels were closed).
Unfortunately, Nuke doesn't really give you any hooks to implement when your widget is being removed from the UI, so it's tough to have a sure-fire way of knowing it's time to unregister your callbacks. The closest you can get is probably using a showEvent/hideEvent combo and some guesswork, like so:
class Info(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.initUI()
self.callbacksRegistered = False
def addCallbacks(self):
nuke.addOnScriptSave(self.updateInfo)
if nuke.root().name() == 'Root' and not nuke.modified():
# No reason to add a scriptLoad callback if opening a
# script would spawn a new Nuke process.
nuke.addOnScriptLoad(self.updateInfo)
def removeCallbacks(self):
self.removeOnScriptSave(self.updateInfo)
self.removeOnScriptLoad(self.updateInfo)
def showEvent(self, event):
if not (self.callbacksRegistered or event.spontaneous()):
self.addCallbacks()
self.callbacksRegistered = True
def hideEvent(self, event):
if self.callbacksRegistered and not event.spontaneous():
# "Spontaneous" hide events are sent when the Nuke
# window is minimized, which we don't really care
# about.
self.removeCallbacks()
self.callbacksRegistered = False
# Rest of the class definition omitted for brevity
The callback registration is moved into the showEvent method, and the callbacks are unregistered when the widget is hidden by something other than an OS-level window operation. This is pretty reliable, except that your callbacks will also be unregistered when you change to another tab in the widget's pane (if it's docked). They will obviously be re-added when your tab is activated again though. This is a pretty minor penalty, but I still think it's worth pointing out that while this approach is pretty close to ideal, it isn't quite perfect.
Anyway, I'll leave it up to you to decide which of these two you like better, but I hope this helps.

How to connect to parent SIGNAL in PyQt?

I have a MainWindow that looks like this:
def __init__(self, parent = None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
self.showMaximized()
menu=mainMenu.MainMenu()
classification=classificationMain.ClassificationMain()
self.stackedWidget.addWidget(menu)
self.stackedWidget.addWidget(classification)
self.stackedWidget.setCurrentWidget(menu)
self.stackedWidget.showFullScreen()
#connections
menu.pushButton.clicked.connect(self.showClassification)
classification.backButton.clicked.connect(self.showMainWindow)
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
def showMainWindow(self):
self.stackedWidget.setCurrentIndex(2)
The MainWindows waits for signal from the rest of the dialogs. Now, the Classification dialog has another StackedWidget in it, since it works as a main window for an important part of the application. It looks like:
class ClassificationMain(QDialog, Ui_Dialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setupUi(self)
choose=choosePatient.ChoosePatient()
self.stackedWidget.addWidget(choose)
self.stackedWidget.setCurrentWidget(choose)
Now, I want to reload the data inside ChoosePatient every time the button "Show Classification" from MainMenu is clicked, but now the data is loaded only once in the line classification=classificationMain.ClassificationMain() of MainWindow.
I was thinking I had to connect a slot inside ChoosePatient with the click of "Show Classification" button inside MainMenu, but I would need an instance of MainMenu, which is not possible.
How can a method of ChoosePatient can be execute every time the button in the "parent" window is clicked? (also, please tell me if this is not the right way to work with pyqt windows)
You need to save references to your composed widgets, and also to expose some public methods to the parents:
class ClassificationMain(QDialog, Ui_Dialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.chooseWidget=choosePatient.ChoosePatient()
self.stackedWidget.addWidget(self.chooseWidget)
self.stackedWidget.setCurrentWidget(self.chooseWidget)
def reloadPatients(self):
# whatever your operation should be on the ChoosePatient
self.chooseWidget.reload()
# MAIN WINDOW
def __init__(self, parent = None):
...
self.classification=classificationMain.ClassificationMain()
self.stackedWidget.addWidget(self.classification)
...
#connections
menu.pushButton.clicked.connect(self.showClassification)
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
self.classification.reloadPatients()
You could also just skip the reloadPatients method and connect to the ChoosePatient directly if you want:
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
self.classification.chooseWidget.reload()
My personal opinion is to make your custom classes wrap up the internal functionality nicely so that you only need to interface with it over the custom class, and not dig into its internals. That way you can change how it works inside without breaking the main window.

Manually toggling of widgets ability to emit signals in pygtk

I'm probably missing something basic in my pygtk programming, but I want to connect a signal to e.g. an gtk.Entry and then make it only emit the connected signal when I allow it to do so.
That is, I want to add something to toggle_signalling in this minimal code (only for interactive use) so that Hello is only printed when signalling is "allowed":
import gtk
signal_states = ['On', 'Off']
signal_state = True
def reporter_function(*args,**kwargs):
print "Hello"
def toggle_signaling(widget, **kwargs):
global signal_state
signal_state = not signal_state
widget.set_label(signal_states[signal_state])
print ['Emit allowed', 'Emit not allowed'][not signal_state]
w = gtk.Window()
e = gtk.Entry()
b = gtk.Button(label=signal_states[signal_state])
hbox = gtk.HBox()
hbox.pack_start(e)
hbox.pack_end(b)
e.connect("changed", reporter_function)
b.connect("clicked", toggle_signaling)
w.add(hbox)
w.show_all()
I've previously let there be a boolean flag for such send signal state e.g. self._updating in my custom widget-classes and let the callback-functions check this state before doing anything. That is not what I want.
I want a gtk-native way of letting the widget know that it shouldn't send the signal (when I've clicked the button in the example). I'm pretty sure I've stumbled upon a way of doing this once but I'm lost at finding it again.
Also, to be absolutely clear, the widget must still be allowed to be enabled.
I don't think there's a way around the boolean flag. Wether a widget is allowed to emit a signal or not is additional application logic and therefore has to be kept somewhere.
Based on your previous research on the topic and the quite acurately described functionality you're most probably looking for chapter 20.1.2 of the PyGTK tutorial.
I put comprehensive example code together. The only thing to keep around except the boolean indicator is the handler_id of the connected signal. As you might notice, it's programmed in Gtk3, but the important methods handler_block and handler_unblock function the exact same way in both Gtk 2 and 3.
from gi.repository import Gtk
class TestWindow(Gtk.Window):
def __init__(self, *args, **kwargs):
Gtk.Window.__init__(self, *args, **kwargs)
self.connect("destroy", Gtk.main_quit)
self.is_allowed = True
self.create_widgets()
self.show_all()
def create_widgets(self):
box = Gtk.HBox()
self.entry = Gtk.Entry()
self.handler_id = self.entry.connect("changed", self.on_entry_changed)
box.pack_start(self.entry, True, True, 0)
button = Gtk.Button("Toggle")
button.connect("clicked", self.on_button_clicked)
box.pack_start(button, True, True, 0)
self.add(box)
def on_entry_changed(self, *args):
print "entry has changed"
def on_button_clicked(self, *args):
if self.is_allowed:
self.entry.handler_block(self.handler_id)
print "now blocking"
else:
self.entry.handler_unblock(self.handler_id)
print "now unblocking"
self.is_allowed = not self.is_allowed
TestWindow()
Gtk.main()

gtk.Builder() and multiple glade files breaks

I have a glade gui, and I want to insert another object using a glade file as well.
When I do it as bellow (this is essentially what I am doing) the whole app hangs and the self.show() and maxes out the CPU at 100%. If I replace the first line of one's init() with self.builder = gtk.Builder() then the app runs, I can set widgets, ie: set contents of entry's, set and change the values of comboboxes. But I cant respond to signals, button clicks never call the handler.
In the real code the object two is set as a page in a note book, and I have multiple other pages, the gtk.main() is in the object that owns the notebook. All these work as expected, it's just the object one that fails.
Any clues? I have tried calling self.builder.connect_signals() for every widget but it still fails to notice them.
class one(gtk.VBox):
def __init__(self, builder):
gtk.VBox.__init__(self)
self.builder = builder # if this is self.builder = gtk.Builder() app runs but widget signals go missing.
self.builder.add_from_file("ui_for_one.glade")
self.show() # Endless loop here?
class two(object): # This is the page in a notebook.
def __init__(self):
self.builder = gtk.Builder()
self.builder.add_from_file("ui_for_two.glade")
self.some_container = self.builder.get_object("some_container")
self.one = one(self.builder)
self.some_container.pack_start(self.one, False, False)
Is there a good reason for using the same gtk.Builder object in two classes?
This might be the cause of your problem. In your one class, you load a glade file but you never do anything with its widgets. Something like this should work:
class one(gtk.VBox):
def __init__(self):
gtk.VBox.__init__(self)
self.builder = gtk.Builder()
self.builder.add_from_file("ui_for_one.glade")
some_widget = self.builder.get_object("some_widget")
self.add(some_widget)
self.builder.connect_signals(self)
# No reason to call self.show() here, that should be done manually.
#Your callback functions here
class two(object): # This is the page in a notebook.
def __init__(self):
self.builder = gtk.Builder()
self.builder.add_from_file("ui_for_two.glade")
self.some_container = self.builder.get_object("some_container")
self.one = one()
self.some_container.pack_start(self.one, False, False)
self.some_container.show_all() #recursively show some_container and all its child widgets
self.builder.connect_signals(self)
For more info, check out these Glade tutorials.

Categories