Home > Blockchain >  QListView context menu gets wrong context
QListView context menu gets wrong context

Time:07-18

I write a PySide6 application where I have a layout with three QListView widgets next to each other. Each displays something with a different list model, and all shall have a context menu. What doesn't work is that the right list model or list view gets resolved. This leads to the context menu appearing in the wrong location, and also the context menu actions working on the wrong thing.

I have done a right-click into the circled area, the context menu shows up in the right panel:

Screenshot of PySide6 application

This is the first iteration of an internal tool, so it is not polished, and neither did I separate controller and view of the UI. This is a minimal full example:

import sys
from typing import Any
from typing import List
from typing import Union

from PySide6.QtCore import QAbstractItemModel
from PySide6.QtCore import QAbstractListModel
from PySide6.QtCore import QModelIndex
from PySide6.QtCore import QPersistentModelIndex
from PySide6.QtCore import QPoint
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QHBoxLayout
from PySide6.QtWidgets import QListView
from PySide6.QtWidgets import QListWidget
from PySide6.QtWidgets import QMainWindow
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QPushButton
from PySide6.QtWidgets import QVBoxLayout
from PySide6.QtWidgets import QWidget


class PostListModel(QAbstractListModel):
    def __init__(self, full_list: List[str], prefix: str):
        super().__init__()
        self.full_list = full_list
        self.prefix = prefix
        self.endResetModel()

    def endResetModel(self) -> None:
        super().endResetModel()
        self.my_list = [
            element for element in self.full_list if element.startswith(self.prefix)
        ]

    def rowCount(self, parent=None) -> int:
        return len(self.my_list)

    def data(
        self, index: Union[QModelIndex, QPersistentModelIndex], role: int = None
    ) -> Any:
        if role == Qt.DisplayRole or role == Qt.ItemDataRole:
            return self.my_list[index.row()]


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.resize(1200, 500)

        prefixes = ["Left", "Center", "Right"]
        self.full_list = [f"{prefix} {i}" for i in range(10) for prefix in prefixes]

        central_widget = QWidget(parent=self)
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(parent=central_widget)
        central_widget.setLayout(main_layout)
        columns_layout = QHBoxLayout(parent=main_layout)
        main_layout.addLayout(columns_layout)

        self.list_models = {}
        self.list_views = {}
        for prefix in prefixes:
            list_view = QListView(parent=central_widget)
            list_model = PostListModel(self.full_list, prefix)
            self.list_models[prefix] = list_model
            self.list_views[prefix] = list_view
            list_view.setModel(list_model)
            list_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
            list_view.customContextMenuRequested.connect(
                lambda pos: self.show_context_menu(list_view, pos)
            )
            columns_layout.addWidget(list_view)
            print("Created:", list_view)

    def show_context_menu(self, list_view, pos: QPoint):
        print("Context menu on:", list_view)
        global_pos = list_view.mapToGlobal(pos)
        index_in_model = list_view.indexAt(pos).row()
        element = list_view.model().my_list[index_in_model]

        menu = QMenu()
        menu.addAction("Edit", lambda: print(element))
        menu.exec(global_pos)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    retval = app.exec()
    sys.exit(retval)


if __name__ == "__main__":
    main()

When it is run like that, it gives this output:

Created: <PySide6.QtWidgets.QListView(0x55f2dcc5c980) at 0x7f1d7cc9d780>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc64e10) at 0x7f1d7cc9da40>
Created: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>
Context menu on: <PySide6.QtWidgets.QListView(0x55f2dcc6bbe0) at 0x7f1d7cc9dd00>

I think that the issue is somewhere in the binding of the lambda to the signal. It seems to always take the last value. Since I re-assign list_view, I don't think that the reference within the lambda would change.

Something is wrong with the references, but I cannot see it. Do you see why the lambda connected to the context menu signal always has the last list view as context?

CodePudding user response:

This is happening because lambdas are not immutable objects. All non parameter variables used inside of a lambda will update whenever that variable updates.

So when you connect to the slot:

lambda pos: self.show_context_menu(list_view, pos)

On each iteration of your for loop you are setting the current value of the list_view variable as the first argument of the show_context_menu method. But when the list_view changes for the second and proceeding iterations, it is also updating for all the preceding iterations.

Here is a really simple example:

names = ["alice", "bob", "chris"]

funcs = []

for name in names:
    funcs.append(lambda: print(f"Hello {name}"))

for func in funcs:
    func()

Output:

Hello chris
Hello chris
Hello chris
  • Related