PyQt Table Header Context Menu - header

I am working on a QTableView displaying a custom model subclasses from QAbstractTableModel in PyQt4. I need to be able to allow the user to set which column they want to serve as a specific type of data. To achieve this I want to implement a context menu when clicking on the header of a column, allowing options to set it as these types. I'm not sure how to create a context menu like this which can differentiate between different columns. Can anyone point me in the right direction?
Thanks

You can access the information from the header view. You can do something like:
def __init__( self, parent ):
# initialize class
...
# setup menu options
header = self.ui.tree.header()
header.setContextMenuPolicy(Qt.CustomContextMenu)
header.customContextMenuRequested.connect( self.showHeaderMenu )
def showHeaderMenu( self, point ):
column = self.ui.tree.header().logicalIndexAt(point.x())
# show menu about the column
menu = QMenu(self)
menu.addAction('Hide Column')
menu.popup(header.mapToGlobal(pos))

Related

how to sort a list of custom widgets in PyQt5?

I have a custom widget with two labels LabelA and LabelB in it. This is added into a QlistWidget. How to sort the list based LabelA or LabelB depending on the user requirements? I would like to keep labelA and labelB in a tile (styled widget)
I saw this solution on here
class ListWidgetItem(QtGui.QListWidgetItem):
def __lt__(self, other):
return self.text() < other.text() # or whatever
But i would like to pass an additional argument to __lt__

Create telegram bot Keyboard from JSON file depending on user menu selection

I am creating a telegram bot that requires a dynamic menu to be created depending on the user's previous menu selection. The dynamic menu should pull from a json file and a specific key:value. Required keyboard is the KeyboardButton and NOT an InlineKeyboardButton.
Example: User is presented with a menu that is A-F, G-L, M-R, and S-Z. When they select the button A-F I am looking for a dynamic menu to be built from the json file where Name is sorted from A-F. The user would then select a name from the new menu and information would be presented associated with that name.
I am lost on the dynamic menu portion for the af_menu_keyboard. Other menus are no problem and can retrieve what I need from that json file.
Anyone know how to achieve this?
result = os.popen("curl https://api.mainnet.klever.finance.....")
details = json.load(result)
def messageHandler(update: Update, context: CallbackContext):
if "A-F" in update.message.text:
update.message.reply_text(node_menu_message()),
reply_markup=af_menu_keyboard():
if "Back" in update.message.text:
update.message.reply_text(main_menu_message(),
reply_markup=main_menu_keyboard())
def first_menu_keyboard():
buttons = [[KeyboardButton("A-F"), KeyboardButton("G-L"), KeyboardButton("M-R")],
[KeyboardButton("S-Z"),KeyboardButton("Back", calback_data='main')]]
return ReplyKeyboardMarkup(buttons,resize_keyboard=True)
def af_menu_keyboard():
buttons = **** Create menu with multiple values from the API JSON file above ****
return ReplyKeyboardMarkup(buttons,resize_keyboard=True)

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().

Multiple replaceWith<>() actions

I have two views in the tornadofx Kotlin app.
Within View # 1 input for certain initial values is performed, certain calculations are made and results are added to mutableListOf <>().
Then View # 1 is replacedWith View # 2 upon pressing the button "Next). The results of calculation are demonstrated in View # 2 in the set of
fileldset{
field("") {label(SimpleFloatProperty(result1))}
}
When the first replaceWith View # 2 operation is performed everything is fine.
But.
In some cases the user has to return to View # 1, change some initial values there, perform the same calculations with the new input and go again to View # 2 to view updated results. I send him to View # 1 with replaceWith command initiated by the button "Back".
He comes back to View # 2 by the button "Next" (= replaceWith View #2).
And here comes the problem. All output (field(""){label (SimpleFloatProperty(result1))}in View # 2 remains the same. They do not change! Despite the fact that the values themselves change OK (debugged that by println of these SimpleFloatProperty).
I tried different workouts - used textfields, for example. It does not help.
I've got a feeling that the fieldset in View is initialized only once - when the View is entered first.
Please, advise how to resolve the described problem.
Thank in advance.

TableView delete column via contextual menu

It is my first time asking here, sorry if i do something wrong (also not in my mother tongue).
Recently, i moved from Swing&AWT to JavaFX.
I am discovering the new Table which is quite different from the Swing version. Better i would say, it needs less operation and do more things, but ... lord, it's way more difficult to understand !
I am currently trying to modify the TableView dynamically. While the addColumn method is not a big challenge, i need help for my deleteColumn method :/
Let's talk about my problem :
I have a scene with many components on it (panes, buttons, menus, ...) and one pane (actually an anchorpane) hosts a TableView.
I would like to dynamically delete an entire column when this operation occurs :
The user right clicks on the TableView > a contextual menu shows up > he selects the item "delete"
So, basically a contextual menu that offers the option to delete the column where the user right-clicked.
I tried this :
-> When the user right-clicks on the TableView, this method is called :
public void setTargetForContext(ContextMenuEvent event){
if(event.getTarget() instanceof Label){
ObservableList list =(((Label)event.getTarget()).getChildrenUnmodifiable());
activeColumn = ((Text)((ObservableList)list)).getText();
}...
And the goal was to set the column name in "activeColumn".
Then, when the user will select the "delete" option from the contextual menu, another method would be called to compare the name of the columns and delete the right one.
But it seems that i can't call a getChildren() method on the label, only an unmodifiable one. And it does not allow a cast and throw the exception.
Do you have a solution to allow me to get the column name ?
Or maybe i am going the wrong way and i have to find another way to delete the right-clicked column, but in this case i will need your help too.
Thanks a lot for reading, and thanks in advance for your help.
First, let me point out that if you call
table.setTableMenuButtonVisible(true);
then the table will have a built-in menu button with radio buttons allowing the user to select which columns are displayed. Maybe this is all you need.
In Swing, the renderers for table cells are just "rubber stamps" that are painted onto the table. Thus you can't register listeners for UI events with them.
By contrast, in JavaFX, the cells in a table are real UI controls with full functionality. This means there's no real need for API that gets the cell coordinates from a table. You should not register your listener with the TableView, but with the actual cells on which you want to operate. You access the cells from the table column's cell factory.
// the table:
TableView<RowDataType> table = new TableView<>();
//...
// A table column:
TableColumn<RowDataType, CellDataType> column = new TableColum<>("Header text");
// A context menu for the table column cells:
ContextMenu contextMenu = new ContextMenu();
MenuItem deleteColumnItem = new MenuItem("Remove Column");
deleteColumnItem.setOnAction(e -> table.getColumns().remove(column));
contextMenu.getItems().add(deleteColumnItem);
// Cell factory for the column
column.setCellFactory(col -> {
// basically a cell with default behavior:
TableCell<RowDataType, CellDataType> cell = new TableCell<RowDataType, CellDataType>() {
#Override
public void updateItem(CellDataType item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText(null);
} else {
setText(item.toString());
}
}
});
// add the context menu to the cell:
cell.setContextMenu(contextMenu);
return cell ;
});
If you want the context menu to appear in the table column header as well, you just need to do
column.setContextMenu(contextMenu);