2

I have a Qmenu that I am creating by loading a list with Qsettings and I am trying to be able to remove items from the menu by loading the list in a QListQWidget and deleting the selected items. Currently I am able to delete the menu items in the list widget and it removes them from qsettings as well but I can't figure out how to remove the items from the menu without restarting. I have tried various things such as removeAction etc but haven't been able to figure it out.
Here is my code:

import functools
import sys
from PyQt5 import QtCore
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, \
    QApplication, QAction, QMenu, QListWidgetItem, \
    QListWidget, QGridLayout

class MainWindow(QWidget):
    settings = QtCore.QSettings('test_org', 'my_app')
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.layout = QHBoxLayout()
        self.menu_btn = QPushButton()
        self.menu = QMenu()
        self.add_menu = self.menu.addMenu("Menu")
        self.menu_btn.setMenu(self.menu)
        self.open_list_btn = QPushButton('open list')

        self.load_items = self.settings.value('menu_items', [])
        for item in self.load_items:
            self.action = QAction(item[0], self)
            self.action.setData(item)
            self.add_menu.addAction(self.action)
            self.action.triggered.connect(functools.partial(self.menu_clicked, self.action))

        self.layout.addWidget(self.menu_btn)
        self.layout.addWidget(self.open_list_btn)
        self.setLayout(self.layout)
        self.open_list_btn.clicked.connect(self.open_window)

    def open_window(self):
        self.create_menu_item = List()
        self.create_menu_item.show()

    def menu_clicked(self, item):
        itmData = item.data()
        print(itmData)

class List(QWidget):
    settings = QtCore.QSettings('test_org', 'my_app')
    def __init__(self, parent=None):
        super(List, self).__init__(parent)
        self.menu_items = self.settings.value('menu_items', [])
        self.layout = QGridLayout()
        self.list = QListWidget()
        self.remove_btn = QPushButton('Remove')
        self.layout.addWidget(self.list, 1, 1, 1, 1)
        self.layout.addWidget(self.remove_btn, 2, 1, 1, 1)
        self.setLayout(self.layout)
        self.remove_btn.clicked.connect(self.remove_items)

        for item in self.menu_items:
            self.item = QListWidgetItem()
            self.item.setText(str(item[0]))
            self.list.addItem(self.item)

    def remove_items(self):
        self.menu_items = self.settings.value('menu_items', [])
        del self.menu_items[self.list.currentRow()]
        self.settings.setValue('menu_items', self.menu_items)
        listItems = self.list.selectedItems()
        if not listItems: return
        for item in listItems:
            self.list.takeItem(self.list.row(item))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    app.exec_()

Does anyone have any ideas?

EDIT:

This is the structure of the list in QSettings. I load the menu with this and I load the QlistWidget with this. I am trying to get the menu to remove the items as well when I remove them for the QListWidget.

mylist = ['item_name',['itemdata1', 'itemdata2', 'itemdata3'], 
          'item_name2',['itemdata1', 'itemdata2', 'itemdata3'], 
          'item_name3',['itemdata1', 'itemdata2', 'itemdata3']]
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Richard
  • 445
  • 1
  • 5
  • 21
  • you can show the config file – eyllanesc Jul 13 '18 at 23:46
  • I am currently just adding lists to the registry with qsettings manually. They look like this mylist = ['item_name',['itemdata1', 'itemdata2', 'itemdata3']] – Richard Jul 14 '18 at 00:01
  • pretty much just an item name for the first item and then a list with random data. I have it sort of figured out using objectnames but it still isn't working too well for me trying that method. – Richard Jul 14 '18 at 00:02
  • From what I understand you have a QListWidget with the names of the QAction, you want that if you remove some QListWidgetItem it will remove its corresponding QAction without needing to restart the application, am I correct? – eyllanesc Jul 14 '18 at 00:05
  • Yes that is correct. When I remove from the list I'd like the menu to update to reflect the removal as well. – Richard Jul 14 '18 at 00:06
  • Each QAction has associated a list that is the data, am I correct? – eyllanesc Jul 14 '18 at 00:07
  • Yes. It is the same data. the menu is loaded by the list in qsettings and the listwidget is loaded with same data from qsettings. – Richard Jul 14 '18 at 00:16
  • I just updated my original post to show how my list is stored. – Richard Jul 14 '18 at 00:22

2 Answers2

3

I think the data structure that you are using is incorrect because when I execute your code it generates twice as many QActions, the structure I propose is a dictionary where the keys are the name of the QAction and the value of the list of data:

{
 'item0': ['itemdata00', 'itemdata01', 'itemdata02'],
 'item1': ['itemdata10', 'itemdata11', 'itemdata12'],
  ...
}

To build the initial configuration use the following script:

create_settings.py

from PyQt5 import QtCore

if __name__ == '__main__':
    settings = QtCore.QSettings('test_org', 'my_app')
    d = {}
    for i in range(5):
        key = "item{}".format(i)
        value = ["itemdata{}{}".format(i, j) for j in range(3)]
        d[key] = value
    settings.setValue('menu_items', d)
    print(d)
    settings.sync()

On the other hand I think that the widget that you want to handle the destruction of QActions should take over the corresponding QMenu as I show below:

import sys
from PyQt5 import QtCore, QtWidgets

class MainWindow(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        layout = QtWidgets.QHBoxLayout(self)

        menu_btn = QtWidgets.QPushButton()
        open_list_btn = QtWidgets.QPushButton('open list')
        layout.addWidget(menu_btn)
        layout.addWidget(open_list_btn)

        menu = QtWidgets.QMenu()
        menu_btn.setMenu(menu)

        self.menu_manager = MenuManager("menu_items", "Menu")
        menu.addMenu(self.menu_manager.menu)
        self.menu_manager.menu.triggered.connect(self.menu_clicked)
        open_list_btn.clicked.connect(self.menu_manager.show)

    def menu_clicked(self, action):
        itmData = action.data()
        print(itmData)


class MenuManager(QtWidgets.QWidget):
    def __init__(self, key, menuname, parent=None):
        super(MenuManager, self).__init__(parent)
        self.settings = QtCore.QSettings('test_org', 'my_app')
        self.key = key

        self.layout = QtWidgets.QVBoxLayout(self)
        self.listWidget = QtWidgets.QListWidget()
        self.remove_btn = QtWidgets.QPushButton('Remove')
        self.layout.addWidget(self.listWidget)
        self.layout.addWidget(self.remove_btn)
        self.remove_btn.clicked.connect(self.remove_items)

        self.menu = QtWidgets.QMenu(menuname)

        load_items = self.settings.value(self.key, [])
        for name, itemdata in load_items.items():
            action = QtWidgets.QAction(name, self.menu)
            action.setData(itemdata)
            self.menu.addAction(action)

            item = QtWidgets.QListWidgetItem(name)
            item.setData(QtCore.Qt.UserRole, action)
            self.listWidget.addItem(item)

    def remove_items(self):
        for item in self.listWidget.selectedItems():
            it = self.listWidget.takeItem(self.listWidget.row(item))
            action = it.data(QtCore.Qt.UserRole)
            self.menu.removeAction(action)
        self.sync_data()

    def sync_data(self):
        save_items = {}
        for i in range(self.listWidget.count()):
            it = self.listWidget.item(i)
            action = it.data(QtCore.Qt.UserRole)
            save_items[it.text()] = action.data()

        self.settings.setValue(self.key, save_items)
        self.settings.sync()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I just posted my answer but I will take a few minutes to go over yours. – Richard Jul 14 '18 at 01:34
  • @Richard As noted in the comments, your method may fail. – eyllanesc Jul 14 '18 at 01:36
  • Yeah I understand that, I wasn't sure anyone was going to answer and posted my answer like 9 seconds after you. I appreciate your help and I am checking out your answer right now. – Richard Jul 14 '18 at 01:38
  • 1
    @Richard wait 2 hours is a short time to think that nobody will respond, on the other hand publish an answer is always good, but it is also better to point out the limitations so the one who wants to use your solution will know what can fail. If my answer helps you, do not forget to mark it as correct :) – eyllanesc Jul 14 '18 at 01:41
  • Thanks for you help, Much better answer than mine. – Richard Jul 14 '18 at 01:50
0

I got it figured out. I am not sure of a better way but I did it using object names.
In the MainWindow I set objectNames to self.action using the first item of each list in the list of lists inside the for loop like this:

self.action.setObjectName(item[0])

Then I created this function in the MainWindow class:

def remove_menu_item(self, value):
    self.add_menu.removeAction(self.findChild(QAction, value))

Then I added this:

w.remove_menu_item(item.text())

To the remove function in the List class to get the same first item in the list of lists which is now the objectName for the QActions.

Richard
  • 445
  • 1
  • 5
  • 21
  • using your method may fail, for example when you have a QMenu that has QAction with the same text as another QAction that belongs to another QMenu. – eyllanesc Jul 14 '18 at 01:35