How to change right-arrow icon to access hidden menu items in QMenuBar using PyQt5? - stylesheet

I found no references about this in the documentation.

You cannot easily style this "extension" button because that symbol is actually an icon.
You can however access the QToolButton widget to set the icon to whatever you like. In PyQt4 you get to it with menubar.children()[0]. This should be consistent with PyQt5. Looking at the Qt5 source code, it appears that the extension icon is always created first, even if it is not shown, and the children() method returns objects in the order in which they were created (this the index of 0).
Once you have a reference to the QToolButton, you can then set the icon to whatever you like with menubar.children()[0].setIcon(my_qicon) (or similar).

Since this is one of the top items on google for modifying the "show more" icon:
Another option is to use a QToolbar. You can do the same thing except that the first child is a layout, second is the QToolButton that you want:
from qtpy import QtWidgets, QtGui
import sys
def call_back():
print('pressed')
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QGridLayout(widget)
toolbar = QtWidgets.QToolBar()
layout.addWIdget(toolbar)
# add some actions
for i in range(10):
toolbar.addAction('test_{}'.format(i), call_back)
# change the icon, the first child is a layout!, the second it the toolbtn we want!
toolbar.children()[1].setIcon(QtGui.QIcon('path/to/image.png'))
widget.show()
app.exec_()
sys.exit()

Related

How do I resize an array of squished PyQt5 widgets? [duplicate]

I have a QScrollArea Widget, which starts empty;
It has a vertical layout, with a QGridLayout, and a vertical spacer to keep it at the top, and prevent it from stretching over the whole scroll area;
Elsewhere in the program, there is a QTextEdit, which when changed, has its contents scanned for "species" elements, and then they are added to the QGridLayout. Any species elements which have been removed are removed too. This bit works;
I have turned the vertical scrollbar on all the time, so that when it appears it does not sit on top of the other stuff in there. Note that the scroll bar is larger than the scroll box already though, despite not needing to be.
This is the problem. The scroll area seems to be preset, and i cannot change it. If i add more rows to the QGridLayout, the scroll area doesn't increase in size.
Instead, it stays the same size, and squeezes the QGridLayout, making it look ugly (at first);
And then after adding even more it becomes unusable;
Note that again, the scroll bar is still the same size as in previous images. The first two images are from Qt Designer, the subsequent 3 are from the program running.
If I resize the window so that the QScrollArea grows, then I see this:
Indicating that there's some layout inside the scroll area that is not resizing properly.
My question is; what do I need to do to make the scrollable area of the widget resize dynamically as I add and remove from the QGridLayout?
If you're coming here from Google and not having luck with the accepted answer, that's because you're missing the other secret invocation: QScrollArea::setWidget. You must create and explicitly identify a single widget which is to be scrolled. It's not enough to just add the item as a child! Adding multiple items directly to the ScrollArea will also not work.
This script demonstrates a simple working example of QScrollArea:
from PySide.QtGui import *
app = QApplication([])
scroll = QScrollArea()
scroll.setWidgetResizable(True) # CRITICAL
inner = QFrame(scroll)
inner.setLayout(QVBoxLayout())
scroll.setWidget(inner) # CRITICAL
for i in range(40):
b = QPushButton(inner)
b.setText(str(i))
inner.layout().addWidget(b)
scroll.show()
app.exec_()
The documentation provide an answer :
widgetResizable : bool
This property holds whether the scroll area should resize the view widget.
If this property is set to false (the default), the scroll area honors the size of its widget.
Set it to true.
Why don't you use a QListView for your rows, it will manage all the issues for you? Just make sure that after you add it you click on the Class (top right window of designer) and assign a layout or it wont expand properly.
I use a QLIstWidget inside a QScrollArea to make a scrollable image list
Try this for adding other objects to the list, this is how I add an image to the list.
QImage& qim = myclass.getQTImage();
QImage iconImage = copyImageToSquareRegion(qim, ui->display_image->palette().color(QWidget::backgroundRole()));
QListWidgetItem* pItem = new QListWidgetItem(QIcon(QPixmap::fromImage(iconImage)), NULL);
pItem->setData(Qt::UserRole, "thumb" + QString::number(ui->ImageThumbList->count())); // probably not necessary for you
QString strTooltip = "a tooltip"
pItem->setToolTip(strTooltip);
ui->ImageThumbList->addItem(pItem);
Update on Artfunkel's answer:
Here's a PySide6 demo that uses a "Populate" button to run the for loop adding items to the scroll area. Each button will also delete itself when clicked.
from PySide6.QtWidgets import *
app = QApplication([])
scroll = QScrollArea()
scroll.setWidgetResizable(True) # CRITICAL
inner = QFrame(scroll)
inner.setLayout(QVBoxLayout())
scroll.setWidget(inner) # CRITICAL
def on_remove_widget(button):
button.deleteLater()
def populate():
for i in range(40):
b = QPushButton(inner)
b.setText(str(i))
b.clicked.connect(b.deleteLater)
inner.layout().addWidget(b)
b = QPushButton(inner)
b.setText("Populate")
b.clicked.connect(populate)
inner.layout().addWidget(b)
scroll.show()
app.exec()

pyqt5 - error zsh: segmentation fault on Mac OS Monterey 12.4 [duplicate]

I am using PyQt based on Qt4. My Editor is PyCharm 2017.3 and my python version is 3.4. I am scraping some text from a website. I am trying to align that text to the center of the cell in a QTableWidget.
item = QTableWidgetItem(scraped_age).setTextAlignment(Qt.AlignHCenter)
self.tableWidget.setItem(x, 2,item)
Therefore while putting the item in the cell, I am trying to align it as per the documentation. The problem is that the data is not showing up.
It did show up when I removed setTextAlignment method as shown below
item = QTableWidgetItem(scraped_age)
self.tableWidget.setItem(x, 2,item)
This line of code:
item = QTableWidgetItem(scraped_age).setTextAlignment(Qt.AlignHCenter)
will not work properly, because it throws away the item it creates before assigning it to the variable. The variable will in fact be set to None, which is the return value of setTextAlignment(). Instead, you must do this:
item = QTableWidgetItem(scraped_age) # create the item
item.setTextAlignment(Qt.AlignHCenter) # change the alignment
This didn't work for me, and I'm not sure if it is because I'm using PyQt5 or it i did something wrong. I was trying to find something similar but for the whole table, and i finally stumbled upon something that worked and lets you center every cells or just one column at a time.
You have to use the delegate method:
#You're probably importing QtWidgets to work with the table
#but you'll also need QtCore for the delegate class
from PyQt5 import QtCore, QtWidgets
class AlignDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(AlignDelegate, self).initStyleOption(option, index)
option.displayAlignment = QtCore.Qt.AlignCenter
After implementing this in your code, you can add the following to your main window class or wherever the table is defined:
delegate = AlignDelegate(self.tableWidget)
self.tableWidget.setItemDelegateForColumn(2, delegate) #You can repeat this line or
#use a simple iteration / loop
#to align multiple columns
#If you want to do it for all columns:
#self.tableWidget.setItemDelegate(delegate)
Know this is an old question, but hope it can help someone else.
Bit late to the party but for those of you wondering how to do this on pyqt5
table = QTableWidgetItem() #QTWidgets.QTableWidgetItem() if importing QWidget from PyQt5
table.setTextAlignment(number)
setTextAlignment takes an int for the argument (alignment). Put the number in to get the result:
0:left
1:left
2:right
3:right
4:centre

How to resize QTableWidgetItem to its editor size then to its text size using QStyledItemDelegate?

I created my own CustomDelegate class derived from QStyledItemDelegate:
class CustomDelegate(QStyledItemDelegate):
def __init__(self, parent):
super().__init__(parent)
def createEditor(self, parent, option, index):
editor = QWidget(parent)
editor_hlayout = QHBoxLayout(editor)
button = QPushButton()
line_edit = QLineEdit()
editor_hlayout.addWidget(button)
editor_hlayout.addWidget(line_edit)
return editor
def setEditorData(self, editor, index):
model_data = index.model().data(index, Qt.EditRole)
editor.layout().itemAt(1).widget().setText(model_data) # Set line_edit value
def setModelData(self, editor, model, index):
editor_data = editor.layout().itemAt(1).widget().text() # Get line_edit value
model.setData(index, editor_data, Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
I set it for the last column of the QTableWidget my_table :
my_delegate = CustomDelegate(my_window)
my_table.setItemDelegateForColumn(my_table.columnCount()-1, my_delegate)
To be precise, my goal is to edit the table widget item size when double-clicking it so that it fits the editor size and properly displays it, then edit the table widget item size right after exiting editor mode so that it fits its text size back again.
To that end, I added the lines index.model().setData(index, editor.sizeHint(), Qt.SizeHintRole) in createEditor method and model.setData(index, QTableWidgetItem(editor_data).sizeHint(), Qt.SizeHintRole) in setModelData method.
The problem is that QTableWidgetItem(editor_data).sizeHint() returns (-1, -1) size. I also tried with QTextDocument(editor_data).size() but it does not fit the text width (it is slightly smaller).
How to get the good text size hint ?
Also, somebody told me about reducing the editor minimum size hint to avoid editing the QTableWidgetItem size at all. Does it solve my problem and how to perform it ?
The editor of a item view should not change the size of its index. The only cases for which this is considered valid is for persistent editors and index widgets, but due to their nature it makes sense: they are expected to persist on the view, and not only it's acceptable that they require the view to eventually expand their row or column, but also necessary in order to avoid editors hiding other items (or editors).
Any change in the section sizes can be potentially very demanding to the view, especially if it has lots of data and any of the header uses the ResizeToContents mode, that's why the default factory editors (QLineEdit, QDate/TimeEdit and Q[Double]SpinBox) don't update the index sizes but eventually extend their geometry temporarily.
I would suggest to follow this practice, and eventually update the geometry in updateEditorGeometry() according to the editor position.
In order to optimize the available space, you could use some precautions:
explicitly set 0 margins for the layout;
minimize the spacing between widgets;
disable the frame of the QLineEdit (as the default editor does);
use QToolButton instead of QPushButton, since it can usually be made smaller than the latter; this also makes it easier some focus aspects, as QToolButton doesn't accept focus by clicking;
Also note that:
the editor should use setAutoFillBackground(True) otherwise parts of the underlying items might be visible;
the line edit should probably have the focus on default, you can use setFocusProxy() on the editor, so that the line edit gets focused when the parent does;
you should not use the layout to access the line edit, instead create a reference for it in the editor object;
if you still want to make the editor border visible, use a proper stylesheet;
def createEditor(self, parent, option, index):
editor = QWidget(parent)
editor.setAutoFillBackground(True)
editor_hlayout = QHBoxLayout(editor)
editor_hlayout.setContentsMargins(0, 0, 0, 0)
editor_hlayout.setSpacing(1)
button = QToolButton()
editor.line_edit = QLineEdit(frame=False)
editor_hlayout.addWidget(button)
editor_hlayout.addWidget(editor.line_edit)
editor.setFocusProxy(editor.line_edit)
# eventually (see note)
editor.setObjectName('delegateEditor')
editor.setStyleSheet('''
#delegateEditor {
border: 1px solid palette(mid);
background: palette(base);
}
''')
return editor
def setEditorData(self, editor, index):
model_data = index.model().data(index, Qt.EditRole)
editor.line_edit.setText(model_data)
def setModelData(self, editor, model, index):
editor_data = editor.line_edit.text()
model.setData(index, editor_data, Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
super().updateEditorGeometry(editor, option, index)
editor.resize(editor.sizeHint().width(), editor.height())
rect = editor.geometry()
parentRect = editor.parent().rect()
if not parentRect.contains(rect):
if rect.right() > parentRect.right():
rect.moveRight(parentRect.right())
if rect.x() < parentRect.x():
rect.moveLeft(parentRect.x())
if rect.bottom() > parentRect.bottom():
rect.moveBottom(parentRect.bottom())
if rect.y() < parentRect.y():
rect.moveTop(parentRect.y())
editor.setGeometry(rect)
Note: if you do use stylesheets, it's possible that the line edit will partially draw over the border; in that case, use editor_hlayout.setContentsMargins(0, 0, 1, 0).
That said, if you really want to update the view sizes, you still can, but it can be a bit tricky.
The trick is to keep track of the editor and its index, manually resize the sections in order to fit its size, and then restore those sizes when the editor is destroyed.
class CustomDelegate(QStyledItemDelegate):
def __init__(self, parent):
self.editorData = {}
super().__init__(parent)
def createEditor(self, parent, option, index):
editor = QWidget(parent)
editor.setAutoFillBackground(True)
editor_hlayout = QHBoxLayout(editor)
editor_hlayout.setContentsMargins(0, 0, 0, 0)
editor_hlayout.setSpacing(1)
button = QToolButton()
editor.line_edit = QLineEdit(frame=False)
editor_hlayout.addWidget(button)
editor_hlayout.addWidget(editor.line_edit)
editor.setFocusProxy(editor.line_edit)
view = option.widget
# store the editor, the view and also the current sizes
self.editorData[index] = (editor, view,
view.horizontalHeader().sectionSize(index.column()),
view.verticalHeader().sectionSize(index.row())
)
# THEN, resize the row and column, which will call sizeHint()
view.resizeColumnToContents(index.column())
view.resizeRowToContents(index.row())
# delay a forced scroll to the index to ensure that the editor is
# visible after the sections have been resized
QTimer.singleShot(1, lambda: view.scrollTo(index))
return editor
def sizeHint(self, opt, index):
if index in self.editorData:
# sizeHint doesn't provide access to the editor
editor, *_ = self.editorData[index]
return editor.sizeHint()
return super().sizeHint(opt, index)
def destroyEditor(self, editor, index):
super().destroyEditor(editor, index)
if index in self.editorData:
editor, view, width, height = self.editorData.pop(index)
view.horizontalHeader().resizeSection(index.column(), width)
view.verticalHeader().resizeSection(index.row(), height)
Note that if the index is near the edge (last row or column), when the editor is destroyed it's possible that the view won't properly adjust its scroll bars if the scroll mode is ScrollPerItem. AFAIK there's no easy workaround for this.
Remember what said above, though. As Ian Malcolm would say, whether this could be done or not, you should stop and think if you should.
Update about focus issues
As pointed out in comments, if the user clicks somewhere else when the editor is active, the editor doesn't close itself as expected.
By default, the delegate's event filter closes the editor whenever it loses focus, but the filter is set on the editor, not its children. When the line edit receives focus, the event filter recognizes that the new focus widget is a child of the editor and will not close it; this is important, because if the editor has child widgets, you don't want it to close just because the internal focus has changed; the side effect of this is that when clicking outside of the line edit, it is the line edit that will receive the FocusOut event, not the editor, so the event filter won't know nothing about it; note that this also happens even when using setFocusProxy(), as Qt automatically sends focus events to the proxy.
There are at least two possible solutions for this, depending on the editor type.
If there is going to be just one main child widget that should accept focus like in this case, the solution is to do what other complex widgets (like QComboBox and QAbstractSpinBox): set the editor as focus proxy of the widget, and post the related events to the widget in the event filter:
def createEditor(self, parent, option, index):
# ...
editor.line_edit.setFocusProxy(editor)
def eventFilter(self, editor, event):
if event.type() in (
event.FocusIn, event.FocusOut,
event.KeyPress, event.KeyRelease,
event.ShortcutOverride, event.InputMethod,
event.ContextMenu
):
editor.line_edit.event(event)
if event.type() != event.FocusOut:
return event.isAccepted()
return super().eventFilter(editor, event)
For more complex situations, we need to ensure that the default implementation of the delegate event filter receives a focus out event whenever the focus is completely lost by the editor or any of its children.
In order to achieve this, a possible solution is to create a custom object that will become an event filter for all child widgets of the editor and eventually posts a FocusOut event for the editor whenever the focused widget is not itself or one of its children (including grandchildren).
class FocusOutFilter(QObject):
def __init__(self, widget):
super().__init__(widget)
self.widget = widget
self.install(widget)
def install(self, widget):
widget.installEventFilter(self)
for child in widget.findChildren(QWidget):
child.installEventFilter(self)
def eventFilter(self, obj, event):
if (event.type() == event.FocusOut and
not self.widget.isAncestorOf(QApplication.focusWidget())):
obj.removeEventFilter(self)
self.deleteLater()
return QApplication.sendEvent(self.widget, event)
elif event.type() == event.ChildAdded:
self.install(event.child())
return super().eventFilter(obj, event)
class CustomDelegate(QStyledItemDelegate):
# ...
def createEditor(self, parent, option, index):
editor = QWidget(parent)
editor.filter = FocusOutFilter(editor)
# ...
There is a catch. The problem is that focusWidget() will return the widget even if the event results in opening a menu. While this is fine for any context menu of the editor widgets (including the edit menu of QLineEdit), it could create some issues when the view implements the context menu event (showing a menu in contextMenuEvent(), within a ContextMenu event or with CustomContextMenu policy). There is no final solution for this, as there is no definite way to check what created the menu: a proper implementation expects that a QMenu is created with the parent that created it, but that cannot be certain (for simplicity, dynamically created menus could be created without any parent, which makes it impossible to know what created them). In these cases, the simplest solution is to implement the above conditions by first checking the return value of isPersistentEditorOpen() and ensure that widgetAt() properly returns the table's viewport().

Keyboard-free mouse gestures for XMonad

I'm setting up mouse gestures in my xmonad.hs, and I'd like to avoid using a modMask modifier to get keyboard-free gestures. Problem is, there are cases (e.g. empty gesture, just a simple click) where I'd like to let the mouse event go through to the application under the cursor, but I haven't found a way to do that in XMonad. Without that, if i add ((0, button3), mouseGesture gestures), i completely lose the "application-specific" functionality of button3.
I was thinking that, if the events cannot be easily forwarded, perhaps a new one can be created and dispatched. I thought about using spawn "xdotool click 3" but it doesn't seem to work. Has anybody ever tried something similar? Otherwise, I'd need a mouse with at least 4 buttons, since most apps actively use three...
To get xdotool to forward your mouse clicks, you need to make sure that it targets the correct window using the --window parameter. For example, if your window id is 79693135, then xdotool click --window 79693135 3 does exactly what you want.
The following basic xmonad.hs illustrates how to do this in order to get keyboardless mouse gestures working properly:
import XMonad
import qualified XMonad.Actions.CycleWS as Workspace
import XMonad.Actions.MouseGestures ( mouseGesture, Direction2D(..) )
import qualified XMonad.Util.EZConfig as EZ
import XMonad.Util.Run ( safeSpawn )
import qualified Data.Map as Map
mouseGestureButton :: Button
mouseGestureButton = button3 -- right click
sendMouseClickToWindow :: Window -> X ()
sendMouseClickToWindow win =
safeSpawn
"xdotool" ["click", "--window", show win, show mouseGestureButton]
myMouseGestures :: [([Direction2D], Window -> X ())]
myMouseGestures =
[ ([R], const Workspace.nextWS) -- move to the next workspace
, ([L], const Workspace.prevWS) -- move to the previous workspace
, ([], sendMouseClickToWindow)
]
addMouseGestures :: XConfig a -> XConfig a
addMouseGestures =
flip EZ.additionalMouseBindings
[((0, mouseGestureButton), mouseGesture $ Map.fromList myMouseGestures)]
main = xmonad $ addMouseGestures def
N.B.: It's not a good idea to use button 1 for this, because that will interfere with selecting text and other mouse-dragging functionality normally used with button 1. So using button 3 (as the OP did) is definitely the right idea.

PyGTK: dynamic label wrapping

It's a known bug/issue that a label in GTK will not dynamically resize when the parent changes. It's one of those really annoying small details, and I want to hack around it if possible.
I followed the approach at 16 software, but as per the disclaimer you cannot then resize it smaller. So I attempted a trick mentioned in one of the comments (the set_size_request call in the signal callback), but this results in some sort of infinite loop (try it and see).
Does anyone have any other ideas?
(You can't block the signal just for the duration of the call, since as the print statements seem to indicate, the problem starts after the function is left.)
The code is below. You can see what I mean if you run it and try to resize the window larger and then smaller. (If you want to see the original problem, comment out the line after "Connect to the size-allocate signal", run it, and resize the window bigger.)
The Glade file ("example.glade"):
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy project-wide -->
<widget class="GtkWindow" id="window1">
<property name="visible">True</property>
<signal name="destroy" handler="on_destroy"/>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">In publishing and graphic design, lorem ipsum[p][1][2] is the name given to commonly used placeholder text (filler text) to demonstrate the graphic elements of a document or visual presentation, such as font, typography, and layout. The lorem ipsum text, which is typically a nonsensical list of semi-Latin words, is a hacked version of a Latin text by Cicero, with words/letters omitted and others inserted, but not proper Latin[1][2] (see below: History and discovery). The closest English translation would be "pain itself" (dolorem = pain, grief, misery, suffering; ipsum = itself).</property>
<property name="wrap">True</property>
</widget>
</child>
</widget>
</glade-interface>
The Python code:
#!/usr/bin/python
import pygtk
import gobject
import gtk.glade
def wrapped_label_hack(gtklabel, allocation):
print "In wrapped_label_hack"
gtklabel.set_size_request(allocation.width, -1)
# If you uncomment this, we get INFINITE LOOPING!
# gtklabel.set_size_request(-1, -1)
print "Leaving wrapped_label_hack"
class ExampleGTK:
def __init__(self, filename):
self.tree = gtk.glade.XML(filename, "window1", "Example")
self.id = "window1"
self.tree.signal_autoconnect(self)
# Connect to the size-allocate signal
self.get_widget("label1").connect("size-allocate", wrapped_label_hack)
def on_destroy(self, widget):
self.close()
def get_widget(self, id):
return self.tree.get_widget(id)
def close(self):
window = self.get_widget(self.id)
if window is not None:
window.destroy()
gtk.main_quit()
if __name__ == "__main__":
window = ExampleGTK("example.glade")
gtk.main()
Here's a one-line variation on killown's solution:
label.connect('size-allocate', lambda label, size: label.set_size_request(size.width - 1, -1))
The above will make sure that the label takes on the width allocated to it, so that word-wrapping is happy.
Not sure why there's a "-1" for the width, but it seems harmless!
VMware's libview has a widget called WrapLabel which should do what you want, but it's in C++. A Python translation is available in the Meld repository (separated out from busybox.py).
example for resize and wrap the label dynamically:
EDIT:
import gtk
class DynamicLabel(gtk.Window):
def __init__(self):
gtk.Window.__init__(self)
self.set_title("Dynamic Label")
self.set_size_request(1, 1)
self.set_default_size(300,300)
self.set_position(gtk.WIN_POS_CENTER)
l = gtk.Label("Dynamic Label" * 10)
l.set_line_wrap(True)
l.connect("size-allocate", self.size_request)
vbox = gtk.VBox(False, 2)
vbox.pack_start(l, False, False, 0)
self.add(vbox)
self.connect("destroy", gtk.main_quit)
self.show_all()
def size_request(self, l, s ):
l.set_size_request(s.width -1, -1)
DynamicLabel()
gtk.main()
You can use this. Not sure where it came from originally. Create your label and then call label_set_autowrap(label)
def label_set_autowrap(widget):
"Make labels automatically re-wrap if their containers are resized. Accepts label or container widgets."
# For this to work the label in the glade file must be set to wrap on words.
if isinstance(widget, gtk.Container):
children = widget.get_children()
for i in xrange(len(children)):
label_set_autowrap(children[i])
elif isinstance(widget, gtk.Label) and widget.get_line_wrap():
widget.connect_after("size-allocate", _label_size_allocate)
def _label_size_allocate(widget, allocation):
"Callback which re-allocates the size of a label."
layout = widget.get_layout()
lw_old, lh_old = layout.get_size()
# fixed width labels
if lw_old / pango.SCALE == allocation.width:
return
# set wrap width to the pango.Layout of the labels
layout.set_width(allocation.width * pango.SCALE)
lw, lh = layout.get_size() # lw is unused.
if lh_old != lh:
widget.set_size_request(-1, lh / pango.SCALE)
In GTK 3, this is done automatically using height-for-width and width-for-height size requests.
I just wanted to share how I made Kai's solution work with PyGtk and Glade-3 using wraplabel.py.
I didn't want to have to modify Glade catalogs to get WrapLabel in Glade and I'm not sure if that would work anyway with a PyGtk component. I was however pleasantly surprised to find that simply by putting the WrapLabel class in the python environment before calling into gtk.Bilder() it will load the class as a component.
So now the only problem was to get the WrapLabels into the glade file. First I changed the names of all the labels I wanted to wrap to wlabel###, where ### is some number. Then I used a sed expression to replace the classes, but since I didn't want to add extra processing to the build system I ended up adding the following in python:
import re
import gtk
from wraplabel import WrapLabel
. . .
# Filter glade
glade = open(filename, 'r').read()
glade = re.subn('class="GtkLabel" id="wlabel',
'class="WrapLabel" id="wlabel', glade)[0]
# Build GUI
builder = gtk.Builder()
builder.add_from_string(glade)
I'm sure there are more elegant ways to do the substitution but this worked well. However, I found I had one more problem. When I opened one of the dialogs with the wrapped labels some of the text was not visible. Then when I resized the window with the mouse, even a little bit, everything would snap in to place. Some how the labels were not getting the right sizes when initialized. I fixed this with another work around. When opening one of the dialogs I run this code:
def open_dialog(self, dialog):
# Hack to make WrapLabel work
dims = dialog.get_size()
dialog.resize(dims[0] + 1, dims[1] + 1)
dialog.present()
dialog.resize(*dims)
This just sets the size one point too big, presents the window and then resets to the correct size. This way the WrapLabels get the signal to resize after the dialog layout is complete.
There is still one small glitch. When you open the dialog sometimes you can see the text snapping in to place. Otherwise, it seems to work.
NOTE 1) All the variations of calling label.set_size_request(size.width - 1, -1) on size-allocate caused the GUI to lockup for me. Probably depends on the parent widgets.
NOTE 2) Another solution is to use TextView's and disable editing, the cursor and sensitivity. However, TextViews have a different color than the background which is difficult to fix in the face of Gtk themes. The other problem with this solution is that TextViews capture mouse scroll events. This makes mouse scrolling a box with these TextViews inside it very erratic. I tried many things to solve the mouse scroll problem but never did figure it out. Otherwise using TextViews does work. So you might consider this if your text labels are not inside a scroll pane and the WrapLabel solution doesn't work for you.
I have modified the code that was in the other answers to get a callback that behaved a little better:
def on_label_size_allocate(self, label, allocation, *args):
""" Callback that re-allocates the size of a label to improve word wrap. """
layout = label.get_layout()
layout.set_width((allocation.width-20) * pango.SCALE)
_, lh = layout.get_pixel_size()
label.set_size_request(-1, lh+6)
The -20 and +6 numbers were obtained by trial and error. It would be nice to get them from somewhere in the widgets, but I couldn't find any relationship to the widgets. This makes the label resize fine both in growing and shrinking and lines are not cut.