GTK / PyGTK make ComboBox searchable with keyboard - pygtk

Is it possible to make a ComboBox searchable ? If yes, how ?
I want to be able, when the ComboBox is active and a letter is typed with the keyboard, to select the first item beginning with this letter inside the ComboBox and so on with the next letters.
The is the same functionality of a ComboBox inside a webpage, for example.
I can't find any option the achieve this on the ComboBox or on the ListStore containing the data in the same way as the TreeView has the methods set_enable_search and set_search_column.

How to: an introduction to GTK+ treeviews and autocompletion.

I finally decided to write my own completion function :
def func(menu, user_data, (widget, window)):
return (widget.get_allocation().x + window.get_position()[0],widget.get_allocation().y + window.get_position()[1],True)
def completion(self, widget, event):
alphanum = re.compile(r'[a-zA-Z0-9-]')
keyval = event.keyval
key = event.string
if keyval == 65288:
#DEL
self.text = self.text[:-1]
elif alphanum.match(key):
self.text = self.text+key
else:
self.yTree.get_widget("comp_menu").popdown()
self.text = ''
return
self.yTree.get_widget("comp_menu").popup( None, None, self.func, 1, event.time, (widget, self.wTree.get_widget('main_window')))
widget.grab_focus()
m = widget.get_model()
j = 0
for i in m:
if i[0].lower().startswith(self.text):
widget.set_active(j)
return
j+=1

Related

Resize border of qgraphicstextitem

I am adding a QGraphicTextItem to a scene using pyqt6.
I cannot resize the widget border when text is resized.
I have looked at a few way of resizing, but none work.
The text does change to a bigger font via the context menu.
The entire class is shown below.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super(FreeTextGraphicsItem, self).__init__(None)
self.x = x
self.y = y
self.text = text_
self.setFlags(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable |
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(self.font)
self.setPlainText(self.text)
self.setPos(self.x, self.y)
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
menu.addAction(_("Large font"))
action = menu.exec(QtGui.QCursor.pos())
if action is None:
return
if action.text() == "Large font":
self.font = QtGui.QFont(self.settings['font'], 12, QtGui.QFont.Weight.Normal)
frame = self.document().documentLayout().frameBoundingRect(self.document().rootFrame())
self.boundingRect().setRect(0, 0, frame.width(), frame.height())
def paint(self, painter, option, widget):
color = QtCore.Qt.GlobalColor.white
painter.setBrush(QtGui.QBrush(color, style=QtCore.Qt.BrushStyle.SolidPattern))
painter.drawRect(self.boundingRect())
painter.setFont(self.font)
fm = painter.fontMetrics()
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
lines = self.text.split('\\n')
for row in range(0, len(lines)):
painter.drawText(5, fm.height() * (row + 1), lines[row])
You're not using the features of QGraphicsTextItem.
In fact, you're completely ignoring and overriding most of its aspects:
x and y are existing and dynamic properties of all QGraphicsItems and should never be overwritten;
the same for font of QGraphicsTextItem;
calling setRect() on the bounding rectangle is useless, as boundingRect() is a *property getter" and is returned internally by the item based on its contents (in this case, the text set with setPlainText());
the text drawing is completely overridden, and not reliable nor consistent with the text set for the item, considering that you're painting the text with split lines, while the original text has escaped new lines;
If your main purpose is to draw a border around the item, then you should only do that, and then rely on the existing capabilities of the item.
class FreeTextGraphicsItem(QtWidgets.QGraphicsTextItem):
def __init__(self, x, y, text_):
super().__init__(text_.replace('\\n', '\n'))
self.setPos(x, y)
self.setFlags(
QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsFocusable
| QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
)
font = QtGui.QFont(self.settings['font'], 9, QtGui.QFont.Weight.Normal)
self.setFont(font)
self.setDefaulTextColor(QtGui.QColor(QtCore.Qt.GlobalColor.white))
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
largeFontAction = menu.addAction(_("Large font"))
action = menu.exec(event.screenPos())
if action == largeFontAction:
font = QtGui.QFont(
self.settings['font'], 12, QtGui.QFont.Weight.Normal)
self.setFont(font)
def paint(self, painter, option, widget=None):
painter.save()
painter.setBrush(QtCore.Qt.GlobalColor.white)
painter.drawRect(self.boundingRect())
painter.restore()
super().paint(painter, option, widget)
Note: comparing actions with their text is pointless, other than conceptually wrong; not only you can have a more reliable object-based comparison using the action (as shown above), but that comparison can also become invalid: a menu could contain items that have the same names, and you're also probably using the _ for translations, so the text might not match at all.

How to display a button in each cell of a QTableWidget's column so that it removes its corresponding row when clicked?

I want to display a button in each cell of a QTableWidget's column. Each button, when clicked, must remove its corresponding row in the table.
To do so, I created a RemoveRowDelegate class with the button as editor and used the QAbstractItemView::openPersistentEditor method in a CustomTable class to display the button permanently.
class RemoveRowDelegate(QStyledItemDelegate):
def __init__(self, parent, cross_icon_path):
super().__init__(parent)
self.cross_icon_path = cross_icon_path
self.table = None
def createEditor(self, parent, option, index):
editor = QToolButton(parent)
editor.setStyleSheet("background-color: rgba(255, 255, 255, 0);") # Delete borders but maintain the click animation (as opposed to "border: none;")
pixmap = QPixmap(self.cross_icon_path)
button_icon = QIcon(pixmap)
editor.setIcon(button_icon)
editor.clicked.connect(self.remove_row)
return editor
# Delete the corresponding row
def remove_row(self):
sending_button = self.sender()
for i in range(self.table.rowCount()):
if self.table.cellWidget(i, 0) == sending_button:
self.table.removeRow(i)
break
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None):
super().__init__(parent)
self.columns = []
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
if df is not None:
self.fill(df)
# Build the table from a pandas df
def fill(self, df):
self.columns = [''] + list(df.columns)
nb_rows, _ = df.shape
nb_columns = len(self.columns)
self.setRowCount(nb_rows)
self.setColumnCount(nb_columns)
self.setHorizontalHeaderLabels(self.columns)
for i in range(nb_rows):
self.openPersistentEditor(self.model().index(i, 0))
for j in range(1, nb_columns):
item = df.iloc[i, j-1]
table_item = QTableWidgetItem(item)
self.setItem(i, j, table_item)
def add_row(self):
nb_rows = self.rowCount()
self.insertRow(nb_rows)
self.openPersistentEditor(self.model().index(nb_rows, 0))
def setItemDelegateForColumn(self, column_index, delegate):
super().setItemDelegateForColumn(column_index, delegate)
delegate.table = self
I set the delegate for the first column of the table and build the latter from a pandas dataframe:
self.table = CustomTable() # Here, self is my user interface
remove_row_delegate = RemoveRowDelegate(self, self.cross_icon_path)
self.table.setItemDelegateForColumn(0, remove_row_delegate)
self.table.fill(df)
For now, this solution does the job but I think of several other possibilities:
Using the QTableWidget::setCellWidget method
Overriding the paint method and catching the left click event
But:
I believe the first alternative is not very clean as I must create the buttons in a for loop and each time a row is added (but after all, I also call openPersistentEditor the same way here).
I am wondering if the second alternative is worth the effort. And if it does, how to do it?
Also:
I believe my remove_row method can be optimized as I iterate over all rows (that is one of the reasons why I thought about the second alternative). Would you have a better suggestion ?
I had to override the setItemDelegateForColumn method so that I can access the table from the RemoveRowDelegate class. Can it be avoided ?
Any other remark that you think might be of interest would be greatly appreciated!
As suggested by #ekhumoro, I finally used a context menu:
class CustomTable(QTableWidget):
def __init__(self, parent=None, df=None, add_icon_path=None, remove_icon_path=None):
super().__init__(parent)
self.add_icon_path = add_icon_path
self.remove_icon_path = remove_icon_path
# Activation of customContextMenuRequested signal and connecting it to a method that displays a context menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(lambda pos: self.show_context_menu(pos))
def show_context_menu(self, pos):
idx = self.indexAt(pos)
if idx.isValid():
row_idx = idx.row()
# Creating context menu and personalized actions
context_menu = QMenu(parent=self)
if self.add_icon_path:
pixmap = QPixmap(self.add_icon_path)
add_icon = QIcon(pixmap)
add_row_action = QAction('Insert a line', icon=add_icon)
else:
add_row_action = QAction('Insert a line')
add_row_action.triggered.connect(lambda: self.insertRow(row_idx))
if self.remove_icon_path:
pixmap = QPixmap(self.remove_icon_path)
remove_icon = QIcon(pixmap)
remove_row_action = QAction('Delete the line', icon=remove_icon)
else:
remove_row_action = QAction('Delete the line')
remove_row_action.triggered.connect(lambda: self.removeRow(row_idx))
context_menu.addAction(add_row_action)
context_menu.addAction(remove_row_action)
# Displaying context menu
context_menu.exec_(self.mapToGlobal(pos))
Moreover, note that using QTableWidget::removeRow method is more optimized than my previous method. One just need to get the row index properly from the click position thanks to QTableWidget::indexAt method.

wxPython using variable to reference an object

I am trying to pass the name of an object into a function so that I can manipulate it from within a function. I have about 14 radio buttons that I want to change depending on what is clicked. Using the function cuts down on code reuse.
However the object tries to use the name of the variable as the object name, not the string contained within.
Is it possible to use this string to refer to the object name compared to the string itself?
self.myObject.GetValue():
self.writeLine('hello','myObject')
self.myObject2.GetValue():
self.writeLine('test','myObject2')
def writeLine(self,str, objName):
print str
self.objName.Enable(False)
self.objName.SetValue(False)
self.objName.Enable(False)
AttributeError: 'myProg' object has no attribute 'objName'
Ignoring the obvious errors in the posted code because you wouldn't be able to run it, if your actual code was as stated, you can pass the object rather than a string.
Then in the function use the object passed not self.objName which doesn't exist!
Here's an utterly pointless piece of code to demonstrate:
import wx
class ButtonFrame(wx.Frame):
def __init__(self, value):
wx.Frame.__init__(self,None)
self.btn1 = wx.Button(self, -1, ("Button A"))
self.btn2 = wx.Button(self, -1, ("Button B"))
self.btnSizer = wx.BoxSizer(wx.HORIZONTAL)
self.btnSizer.Add(self.btn1 , 0, wx.RIGHT, 10)
self.btnSizer.Add(self.btn2 , 0, wx.RIGHT, 10)
self.btn1.Bind(wx.EVT_BUTTON, self.OnPressA)
self.btn2.Bind(wx.EVT_BUTTON, self.OnPressB)
self.SetSizer(self.btnSizer)
self.Centre()
self.Show()
self.btn1.Enable(False)
mystr = self.btn1.GetLabel()
self.writeLine(mystr,self.btn1)
self.writeLine('test',self.btn2)
def writeLine(self,str, objName):
print(str)
x = objName.IsEnabled()
objName.Enable(not x)
def OnPressA(self,evt):
self.btn1.SetLabel('Button C')
self.Layout()
def OnPressB(self,evt):
self.btn2.SetLabel('Button D')
self.Layout()
if __name__ == "__main__":
app = wx.App(False)
ButtonFrame(None)
app.MainLoop()
Initially, as you can see button A is disabled button B is enabled.
The result should be:
Button A is Enabled
Button B is Disabled
Printed on the command line should be Button A followed by test

PyQt5 QTableView selected cell background with Delegate

I have an application which uses a QTableView/QAbstractTableModel combination. For the view, I've defined a Delegate which displays an image (a QPixmap, loaded from an image file) in one column of the table view.
Basically, the problem is that when a cell in the column with the Delegate is selected, sometimes the background shows and sometimes it doesn't.
Here is what I've discovered by experimentation so far, and I can't make much sense of it:
I have this relatively short test program:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsSelectable
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = QtWidgets.QTableView()
tableView.setItemDelegateForColumn(0, Delegate())
tableView.resize(550, 160)
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
When I specify app.setStyle('fusion') in __main__, I get what I would expect: When a cell in the column with the Delegate is selected, the cell background is blue and the image appears in front of it:
However, if I change to app.setStyle('windows'), even though in general it uses the same blue background for selected cells, when I move to a cell in the first column, the background disappears:
(You can't obviously see it, but the same cell is selected as in the first example).
That's just a piece of test code, which I don't completely understand.
In the actual application I'm writing, I am using Qt Designer to create the UI. Even though I specify app.setStyle('fusion'), the table has entirely different styling, with a different appearance to the background of a selected cell:
I can't for the life of me figure out where it is picking up the different style. It must come from Qt Designer somehow, but I've looked at the .py file Qt Designer creates, and I can't find it.
This style (wherever it comes from) seems to suffer from the same problem as the windows style. In the image above, there is no Delegate in use. The cell in row 2/column 2 is selected, and the background shows.
But if I add a Delegate to display a QPixmap in column 2, then the background does not show when the cell is selected:
(It's selected; take my word for it).
I thought maybe it was the case that once you use a Delegate to display an image, you could no longer get a background in the selected cell. But you obviously can. It works in one case, just not the others.
If anyone can shed light on this, I'd appreciate it. (I realize this is long; thanks for sticking with me).
I've been fiddling around with this issue more, and I've learned some things about my original question. In retrospect, I think it was not as clear as it could have been (or maybe I just understand it all a bit better).
For starters, I never should have referred to cells as being "selected". In fact, I don't even have the Qt.ItemIsSelectable flag set for any of the cells in the view. What I really have been trying to do is control the background of a cell when it is active (for lack of a better word) -- meaning it is the cell where the cursor is currently positioned.
This can be done by overriding initStyleOption() in the Delegate. My original test code is modified as shown below:
from PyQt5 import QtCore, QtWidgets, QtGui
import sys
# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = [[]], headers = None, parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__data = data
def rowCount(self, parent):
return len(self.__data)
def columnCount(self, parent):
return len(self.__data[0])
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole:
value = self.__data[row][column]
return value
if role == QtCore.Qt.BackgroundRole:
return QtGui.QBrush(QtGui.QColor(255, 255, 255))
def flags(self, index):
return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable
# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):
# <Modification>
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
option.backgroundBrush = QtGui.QBrush(QtGui.QColor(232, 244, 252))
def paint(self, painter, option, index):
if (index.column() == 0):
# <Modification>
if (
index.row() == tableView.currentIndex().row() and
index.column() == tableView.currentIndex().column()
):
self.initStyleOption(option, index)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(option.backgroundBrush)
painter.drawRect(option.rect)
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
else:
super().paint(painter, option, index)
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('fusion')
tableView = TableView()
tableView.resize(550, 160)
tableView.setItemDelegate(Delegate())
tableView.show()
rowCount = 3
columnCount = 4
data = [
[i for i in range(columnCount)]
for j in range(rowCount)
]
model = TableModel(data)
tableView.setModel(model)
sys.exit(app.exec_())
initStyleOption() sets the background brush for a cell when it is active (current). But as I bemoaned before, this doesn't occur in the first column, which has a Delegate with a custom paint() method that displays a pixmap. So paint() must also take responsibility for setting the background for cells in that column when they are active. It uses the same backgroundBrush that initStyleOption() set.
The result is very nearly what I'm shooting for. The only fly in the ointment is that there is still clearly additional styling going on, that affects all the cells in the view except those in column 1 with the custom Delegate. So they don't look quite exactly alike when active:
(It's subtle, but there's a bit of a gradient to the background of the cell in column 2, which is absent in column 1).
I know now that there are style 'factories' that apply a widget-wide style. Since I'm using Fusion, that is evidently where the extra styling is coming from.
So now my question is -- where is that styling defined, and do I have any control over it? If I could see it, I could make my custom background style match it. Better yet, if I could modify it, I could make it match mine.
I had the same problem with my own tool today. I think your issue is the same as this other question. In short, you just need to call super in paint before doing any of your extra work. When I added super to my own code, selections worked again as expected in the delegate.
class Delegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
if (index.column() == 0):
image = QtGui.QImage('open.png')
pixmap = QtGui.QPixmap.fromImage(image)
x = option.rect.center().x() - pixmap.rect().width() / 2
y = option.rect.center().y() - pixmap.rect().height() / 2
painter.drawPixmap(x, y, pixmap)
(FWIW I haven't tested the code above. But it should work).

How to create a dropdown from a gtk.Entry's icon?

I have a gtk.Entry with an icon after the text, intending to be a text search field:
What I'm trying to do is to display a dropdown (i.e. a gtk.ComboBox) when the user clicks on the icon, to choose the type of search. A mock of that feature would be:
I have tried several things without any success. For example, trying to pack an empty gtk.ComboBox only showing an arrow right after the Entry, and stuffing it only on icon-press, which creates the illusion, but it has two drawbacks: a) when I stuff the ComboBox, the toolbar grows, and b) when I clear() the ListStore, the ComboBox retains its width and leaves an ugly grey box.
At this point I guess that I need to create a CellRenderer on icon-press that pops down the icon of the Entry, and I tried without a lot of success to understand the code of gtk.ComboBoxEntry (in gtkcomboboxentry.c), but as far as I understood it uses a vertical Container on the whole piece together with a CellRenderer.
Also GTK+3 doesn't have any ideas on this respect.
Any ideas, or some guidance in how to create this in PyGTK?
I was looking for something similar, so I came up with the code below. I haven't really worried about the aesthetics. I did pass a list of tuples to the MyPopup class, with the idea of passing handlers for each of the menu items in the dropdown. Note that the item.show() is necessary, even though there is a show_all():
from gi.repository import Gtk
class MyPopup(Gtk.MenuButton):
def __init__(self, btndefs):
super(MyPopup, self).__init__()
self.menu = Gtk.Menu()
self.set_popup(self.menu)
#self.set_label(">")
self.set_direction(Gtk.ArrowType.RIGHT)
for btndef in btndefs:
item = Gtk.MenuItem()
item.set_label(btndef[0])
item.show()
self.menu.append(item)
class MainWindow(Gtk.Window):
def __init__(self):
super(MainWindow, self).__init__()
self.set_size_request(100, -1)
self.connect("destroy", lambda x: Gtk.main_quit())
self.hbox = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL)
self.entry = Gtk.Entry()
self.popup = MyPopup( (("String",),
("String no case",),
("Hexadecimal",),
("Regexp",)) )
self.hbox.pack_start(self.entry, True, True, 0)
self.hbox.pack_start(self.popup, False, True, 0)
self.add(self.hbox)
self.show_all()
def run(self):
Gtk.main()
def main():
mw = MainWindow()
mw.run()
return 0
if __name__ == '__main__':
main()
yup its year late, but lets not make next person stumbled here to be sad like me.
this is the example using Gtk.Menu() popup, you can also similar feat. with Gtk.Popover()
#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
opts = {
'hex' : "system-run-symbolic",
'regex' : "font-select-symbolic",
'string' : "font-x-generic-symbolic",
'no-case' : "tools-check-spelling-symbolic",
}
def make_menu(entry, opts):
menu = Gtk.Menu()
for label, icon in opts.items():
item = Gtk.MenuItem()
item.set_label(label)
item.connect(
"activate",
lambda w: entry.set_icon_from_icon_name(0, opts[w.get_label()])
)
menu.append(item)
# NOTE you can use Gtk.ImageMenuItem to add image but its
# Deprecated since version 3.10
menu.show_all()
return menu
def on_icon_release(widget, pos, event):
menu = make_menu(widget, opts)
menu.popup(
parent_menu_shell = None,
parent_menu_item = None,
func = None,
data = None,
button = Gdk.BUTTON_PRIMARY,
activate_time = event.get_time()
)
def make_entry():
entry = Gtk.Entry()
entry.set_icon_from_icon_name(0, 'action-unavailable-symbolic')
entry.set_icon_from_icon_name(1, 'fonts')
entry.set_icon_sensitive(1, True)
entry.set_icon_activatable(1, True)
entry.connect("icon-release", on_icon_release)
return entry
root = Gtk.Window()
root.add(make_entry())
root.show_all()
Gtk.main()