Search code examples
c++qtlistviewqlistviewqtwidgets

How to make text selectable on a QListView custom widget?


Context

TLTR: For a command-line console widget, I need to be able to select the texts on a QListView row.

In a similar way that your browser command-line (e.g. pressing F12 on Firefox and going to "Console", similar for Chrome, others). I am creating a command-line console for interacting with my application.

enter image description here

Each command and it result is pushed into a list above the input text-box, allowing each item to be drawn nicely and user-friendly:

  • The text goes through a QSyntaxHighlighter
  • Long lines, or multiple lines are elided
  • Results which are objects can be expanded or collapsed
  • etc..

Most of those goals are not yet implemented, but it's clear that I need a custom-widget to represent each row.

enter image description here

Now, I need the text on that QListView item to be selectable, and copiable.

The problem

TLTR: Selecting the texts requiers to enter edit-mode, I don't like having to double-click first for selecting text.

Using a QStyledItemDelegate, Overriding paint: I managed to have a draft of the apparence I need. But that apparence is mostly static, no interaction exists. Overriding createEditor and setEditorData, the content can be edited. I can set the QTextEdit as readOnly and then, it is just selectable as required.

However, the list items needs to be double-clicked, or selected+clicked in order to get into edit-mode, and being able to select the text. But, the user expects to select the text as it is, just by pressing+moving+relasing the mouse.

The text should be selectable as it is, without double-clicking the row for getting into edit-mode

Some code

#include <QApplication>
#include <QListView>
#include <QStyledItemDelegate>
#include <QLabel>
#include <QPainter>
#include <QPaintEvent>
#include <QStandardItemModel>
#include <QTextEdit>

class CommandLineItemDelegate: public QStyledItemDelegate
{
    Q_OBJECT
    mutable QTextEdit m_editor;
public:
    void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QRect rect(QPoint(0, 0), option.rect.size());

        m_editor.setPlainText( index.data().toString());
        m_editor.resize(option.rect.size());

        p->save();
        p->translate(option.rect.topLeft());

        m_editor.render(p, QPoint(), QRegion());
        p->restore();
    }

    QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override
    {
        return QSize(200,50);
    }

    QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        auto* edit = new QTextEdit(parent);
        edit->setGeometry(option.rect);
        edit->setReadOnly(true);

        return edit;
    }

    void setEditorData(QWidget *editor, const QModelIndex &index) const override
    {
        auto* textEditor = dynamic_cast<QTextEdit*>(editor);
        if (textEditor != nullptr)
        {
            textEditor->setPlainText(index.data().toString());
        }
    }

    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override
    {
        auto* textEditor = dynamic_cast<QTextEdit*>(editor);
        if (textEditor != nullptr)
        {
            model->setData(index, textEditor->toPlainText());
        }
    }
};

class CommandLineListView: public QListView
{
    Q_OBJECT
    CommandLineItemDelegate m_delegate;
    QStandardItemModel m_model;
public:
    explicit CommandLineListView( QWidget* parent=nullptr)
        : QListView(parent)
        , m_delegate()
    {
        setModel(&m_model);
        m_model.insertColumn(0);
        m_model.insertRows(0,3);
        m_model.setData(m_model.index(0,0),"var adri = function(a,b){return a+b; }; // function to sum");
        m_model.setData(m_model.index(1,0),"Math.PI");
        m_model.setData(m_model.index(2,0),"2+2");
        setSelectionMode(QAbstractItemView::SelectionMode::NoSelection); //Text selection, but no row selection.

        setItemDelegate(&m_delegate);
    }
};

#include "main.moc"

int main(int argn, char* argv[])
{
    QApplication app(argn, argv);

    CommandLineListView list;
    list.show();

    app.exec();
}

enter image description here

As explained before, this "mostly" works, except that the user needs to double-click the row to enter edit-mode and selecting the text, which is not acceptable.


Solution

  • Quick solution: easy, but not very performant for large list views

    A quick solution is to keep all the editors open, by calling openPersistentEditor for each added row.

    Note that this is not the most performant solution (for very large list view), but may be good enough for your use case.

    Alternative 1: Implement your own QStyledItemDelegate

    This allows full customisation of the formatting, but also requires that you implement the text selection feature yourself.

    Alternative 2: Use HTML

    Displaying it as HTML (which is probably what Chrome and Firefox do) allows you full customisation and using the built-in selection feature.

    Alternative 3: Use QML

    QML is often easier to (rapidly) create a custom user interface.