Keep most recent added combobox item on top between sessions

Hi
using pyside6:
I use a combobox to pick files which are dragged/dropped on the combobox. QComboBox.InsertPolicy.InsertAtTop
is set and all that works fine. Newly dropped files are shown on the top of the picklist.

I want a persistent “most recent files” list between sessions. So I save the items

[items_append(combobox.itemText(i)) for i in range(cnt)]

with pickle to a file.
However the item order is different from the picklist (most recent entry on top). The most recent entry is still the last one in items_append.
How can I change it to be in the same order as shown in the GUI?

@frizzo
Hi, and welcome to the forum. I’ve worked with keeping a persistent recent files list in the past and I think you are on the right track. Without a more detailed code sample, I can not be sure what exactly is wrong with what you’ve been working on, but I have a basic working solution that you should be able to implement.

In the past, I’ve used a YAML file to store the data that I wanted to persist because it allows me to store lots of data in a human readable format. A JSON file would also be quite appropriate. However, since you mentioned that you are using pickle, I’ve used that in my examples. I am using PyQt6 instead of PySide6, but it’s my understanding that all the concepts will transfer directly.

I believe the main thing to keep in mind when wanting to keep your data in the same order when it is saved is to ensure you’re using a container that doesn’t automatically sort it’s entries for you. A list and an OrderedDict are both appropriate and which one you use is solely dependent on how you want to interact with the data. You don’t want to use a standard dict or a set as both of these will alphabetize the entries.

I’ve created two different sample classes which extend QComboBox to do what I believe you are looking for. I also took the liberty of using the ability to add both display text and associated data for each entry to keep the drop-down items more readable.

QComboBox using a list for data storage

import os
import pickle

from PyQt6.QtWidgets import QComboBox


class ComboBoxList(QComboBox):
    def __init__(self, *args, **kwargs):
        QComboBox.__init__(self, *args, **kwargs)
        self.setAcceptDrops(True)
        self.setPlaceholderText("Drag files here...")

        self.pickle_file = "combo_list.pkl"

        # Load data from pickle file and assign it to the combobox
        try:
            with open(self.pickle_file, "rb") as file:
                list_data = pickle.load(file)
            print(f"Data type: {type(list_data)}")
            for text, data in list_data:
                self.addItem(text, data)
        except (FileNotFoundError, pickle.UnpicklingError):
            print("Pickle data not available")

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                file_path = url.toLocalFile()
                if file_path:  # Ensure a valid local file path is obtained
                    file_name = os.path.basename(file_path)
                    self.insertItem(0, file_name, file_path)
                    self.setCurrentIndex(0)
                    self._save_list()
            event.acceptProposedAction()
        else:
            event.ignore()

    def _save_list(self):
        cur_list = [
            (self.itemText(i), self.itemData(i)) for i in range(self.count())
        ]
        with open(self.pickle_file, "wb") as file:
            pickle.dump(cur_list, file)

QComboBox using an OrderedDict for data storage

import os
import pickle
from collections import OrderedDict

from PyQt6.QtWidgets import QComboBox


class ComboBoxOrderedDict(QComboBox):
    def __init__(self, *args, **kwargs):
        QComboBox.__init__(self, *args, **kwargs)
        self.setAcceptDrops(True)
        self.setPlaceholderText("Drag files here...")

        self.pickle_file = "combo_ordered_dict.pkl"

        try:
            with open(self.pickle_file, "rb") as file:
                ordered_dict_data = pickle.load(file)
            print(f"Data type: {type(ordered_dict_data)}")
            for text, data in ordered_dict_data.items():
                self.addItem(text, data)
        except (FileNotFoundError, pickle.UnpicklingError):
            print("Pickle data not available")

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event):
        if event.mimeData().hasUrls():
            for url in event.mimeData().urls():
                file_path = url.toLocalFile()
                if file_path:  # Ensure a valid local file path is obtained
                    file_name = os.path.basename(file_path)
                    self.insertItem(0, file_name, file_path)
                    self.setCurrentIndex(0)
                    self._save_list()
            event.acceptProposedAction()
        else:
            event.ignore()

    def _save_list(self):
        cur_ordered_dict = OrderedDict()
        for i in range(self.count()):
            cur_ordered_dict[f"{self.itemText(i)}"] = self.itemData(i)
        with open(self.pickle_file, "wb") as file:
            pickle.dump(cur_ordered_dict, file)

QMainWindow class to sample their functionality

import sys

from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QVBoxLayout,
    QMainWindow,
    QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Drag and Drop Files to ComboBox")

        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)

        layout.addWidget(
            QLabel(
                "Persistent QComboBox list using a list storage containter:"
            )
        )
        self.combo_box_list = ComboBoxList()
        self.combo_box_list.setObjectName("QComboBox with list")
        self.combo_box_list.currentIndexChanged.connect(
            self.on_list_index_change
        )
        layout.addWidget(self.combo_box_list)

        layout.addWidget(
            QLabel(
                "Persistent QComboBox list using an OrderedDict storage "
                "conatainer:"
            )
        )
        self.combo_box_ordered_dict = ComboBoxOrderedDict()
        self.combo_box_ordered_dict.setObjectName("QComboBox with OrderedDict")
        self.combo_box_ordered_dict.currentIndexChanged.connect(
            self.on_ordered_dict_index_change
        )
        layout.addWidget(self.combo_box_ordered_dict)

    def on_list_index_change(self):
        self._print_combo_box_info(self.combo_box_list)

    def on_ordered_dict_index_change(self):
        self._print_combo_box_info(self.combo_box_ordered_dict)

    def _print_combo_box_info(self, combobox):
        print(f"{combobox.objectName()}; Type: {type(combobox)}; Text:"
              f"{combobox.currentText()}; Data: {combobox.currentData()}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec())

Of course you will want to adjust things like file paths and overall functionality of the QCombBox to suit your needs, but both of the combo box classes will load the data that was saved to the pickle files so they start with the list they had when they closed.

1 Like

Hi Dave
For now many thanks for your time to provide such a detailed answer! I think the “auto sorting container” tip is promising. I’ll check and apply your suggestions to my code and let you know.
Again, thanks.

@DevMolasses

I was saving the current list on the widget closeEvent once. In your example it is saved on every dropEvent and that does the trick! Thanks, again!

Happy to help. I’m glad you got it working. If you prefer to only save once at the end, you have to tie the saving to the application by using “QApplication().instance().aboutToClose.connect()” and point it to the function that will save the list.