How to make text selectable on a QListView custom widget? - c++

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

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.

Related

Save state of QTableWidget cell widget checkbox before setting its state

I have a QTableWidget, where one column is filled with custom checkboxes. Also, I have implemented an undo mechanic so that every change to the table can be undone. For the other item columns, where only text is stored, I basically achieve it in the following way:
Every time an item in the table is pressed (calling the itemPressed signal), I store the table data before the item editing starts using a function called saveOldState. After editing (and triggering an itemChanged signal), I push the actual widget content together with the old content onto a QUndoStack instance using a function called pushOnUndoStack.
Now I want to achieve a similar thing for the cell widgets. However, changing the checkbox state does not trigger itemChanged. Thus, I have to connect to the checkbox's stateChanged signal to save the new state:
QObject::connect(checkBox, &QCheckBox::stateChanged, this, [checkBox] {
pushOnUndoStack();
});
So, getting the newest table data is not that hard. However, I am struggling to find the right moment to save the data before the checkbox is set, because there is no similar variant for an itemPressed signal in case of a cell widget.
My question is: Is there a good alternative way to store the checkbox state immediately before the state is actually set? Currently, my only idea is to implement a custom mouse move event filter for the cell widget, which calls saveOldState the moment a user moves the mouse inside the cell widget's boundaries. But is there maybe a better way?
Chapter 1: the solution you cannot use
What I think is the correct way to address your question is a proxy model in charge of maintaining the undo stack. It does so by saving the model's data right before changing it.
Header:
class MyUndoModel : public QIdentityProxyModel
{
public:
MyUndoModel(QObject* parent = nullptr);
bool setData(const QModelIndex& index, const QVariant& data, int role) override;
bool restoreData(const QModelIndex& index, const QVariant& data, int role);
private:
void pushOnUndoStack(const QPersistentModelIndex& index, int role, const QVariant& value) const;
//QUndoStack undoStack;
};
Source:
bool MyUndoModel::setData(const QModelIndex& index, const QVariant& data, int role)
{
QVariant currentData = index.data(role);
bool result = QIdentityProxyModel::setData(index, data, role);
if (result) {
//If the source model accepted the change, push currentData to the undo stack.
pushOnUndoStack(QPersistentModelIndex(index), role, currentData );
}
return result;
}
bool MyUndoModel::restoreData(const QModelIndex& index, const QVariant& data, int role)
{
return QIdentityProxyModel::setData(index, data, role);
}
Note that we use QPersistentModelIndexes in a modified version of pushOnUndoStack (that I let you implement yourself). Also, I did not write how the stacked undo/redo commands should be processed, apart from calling restoreData. As long as you get the idea...
Chapter 2: where it fails for you
The above solution works regarless of the actual class of the source model ... except if working with QTableWidget and QTreeWidget.
What blocks this solution in the case of e.g. QTableWidget is its internal model (QTableModel).
You cannot substitute model of your QTableWidget to use MyUndoModel instead.
If you try, you will very quickly see your application crash.
You could in theory subclass QTableModel to perform the above substitution but I advise against it.Sample: myTableWidget->QTableView::setModel(new MyQTableModel);QTableModel is a private class in Qt and should not be used directly. I wish I knew why it was done this way.
Chapter 3:The alternative solution
Alternatively, subclassing QStyledItemDelegate could work for you. The design is not as clean, there are more ways to make a mistake when using it in your window but it essentially follows the same logic as the above proxy model.
class UndoItemDelegate : protected QStyledItemDelegate
{
public:
UndoItemDelegate(QUndoStack* undoStack, QObject* parent = nullptr);
//Importnt: we set setModelData as final.
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override final;
protected:
virtual QVariant valueFromEditor(QWidget *editor) const noexcept = 0;
virtual int roleFromEditor(QWidget *editor) const noexcept = 0;
private:
void pushOnUndoStack(const QPersistentModelIndex& index, int role, const QVariant& value) const;
//undoStack as a pointer makes it possible to share it across several delegates of the same view (or of multiple view)
mutable QUndoStack* undoStack;
};
The magic is in setModelData.
void UndoItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
auto role = roleFromEditor(editor);
QVariant currentData = index.data(role);
bool dataChanged = model->setData(index, valueFromEditor(editor), role);
if (dataChanged && undoStack) {
pushOnUndoStack(QPersistentModelIndex(index), role, currentData);
}
}
I kept the version with index (my habit) but you could use the pointers to QTableItem of course.
To be used (most likely in the constructor of your window):
ui->setupUi(this);
auto myDelegate = new MyUndoItemDelegateSubclass(&windowUndoStack, ui->myTableWidget);
ui->myTableWidget->setItemDelegate(myDelegate);
You will have to implement:
pushOnUndoStack (once).
roleFromEditor and valueFromEditor (for every subclass).
the processing of undo/redo commands.
Edit to address your comment.
I am going to assume you know how QAbstractIdemModel and subclasses work in a generic manner. To manipulate a checkState in the model of a QTableWidget, I recommend you create a UndoCheckboxDelegate subclass to implement/override the additional methods this way:
Header:
class UndoCheckboxDelegate : public UndoItemDelegate
{
public:
UndoCheckboxDelegate(QUndoStack* undoStack, QObject* parent = nullptr);
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
protected:
virtual QVariant valueFromEditor(QWidget *editor) const noexcept override;
virtual int roleFromEditor(QWidget *editor) const noexcept override;
};
Source:
UndoCheckboxDelegate::UndoCheckboxDelegate(QUndoStack* undoStack, QObject* parent)
: UndoItemDelegate(undoStack, parent)
{}
QWidget* UndoCheckboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.isValid()) {
QCheckBox* control = new QCheckBox(parent);
control->setText(index.data(Qt::DisplayRole).toString());
control->setCheckState(index.data(Qt::CheckStateRole).value<Qt::CheckState>());
return control;
}
else
return nullptr;
}
QVariant UndoCheckboxDelegate::valueFromEditor(QWidget *editor) const noexcept
{
if (editor)
return static_cast<QCheckBox*>(editor)->checkState();
else
return QVariant();
}
int UndoCheckboxDelegate::roleFromEditor(QWidget * /* unused */) const noexcept
{
return Qt::CheckStateRole;
}
It may be only a starting point for you. Make sure it correctly fills the undo stack first; after that, you can tweak the behavior a bit.

How to catch key presses in editable QTableWidgetItem?

Now I can process all key presses in my QTableWidget in a function eventFilter() (after calling of myTable->viewport()->installEventFilter(this); in the constructor).
The only place where this doesn't work is editable cell while editing (because it grabs all key presses). To fix it I can't call installEventFilter() for each item in the table, because these items are not QObjects (and also I can't use connect for putting of my processing of key presses).
The only solution I have is to put QLineEdits in these cells and to use event filter to catch key presses while editing. But is it possible to solve it using only standard items? (i.e. only QTableWidgetItem with a flag Qt::ItemIsEditable)
Also I can call grabKeyboard() for my QTableWidget. In this case I'll have all key presses (even while editing of cells by user), but it blocks edit box (i.e. user can't input anything). May be it is possible to fix broken edit boxes after calling of grabKeyboard() for the table?
This so quite ease to achieve. Just subclass QStyledItemDelegate override createEditor method like this:
QWidget *AlterEditorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QWidget *result = QStyledItemDelegate::createEditor(parent, option, index);
result->installEventFilter(new YourEventFilter(result));
return result;
}
Than replace delegate for your QTableWidget.
Or even better instead subclassing create proxy class which accepts original QAbstractItemDelegate (more writing but much more universal and can be composed with other modifications).
AlterEditorProxyDelegate::AlterEditorProxyDelegate(QAbstractItemDelegate *original, QObject *parent)
: QAbstractItemDelegate(parent)
, original(original)
{}
QWidget *AlterEditorProxyDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QWidget *result = original->createEditor(parent, option, index);
result->installEventFilter(new YourEventFilter(result));
return result;
}
// other methods which invokes respective methods for `original` style.
Since QTableWidgetItem has no function keyEvent() that you can overload this is not possible.
What you have to do is set a delegate with custom editor factory that produces widgets where keyEvent is overloaded.
But is it possible to solve it using only standard items? (i.e. only QTableWidgetItem with a flag Qt::ItemIsEditable)
Not really. In Qt4 QTableWidget leaks KeyRelease events from the cell editor, but exploiting that would be an ugly hack.
May be it is possible to fix broken edit boxes after calling of grabKeyboard() for the table?
I once tried doing that and then posting the events to QTableWidget but ran into trouble as well.
The proper thing to do is to create your own delegate and install event filter in createEditor function. You can do something like this:
class FilterDelegate : public QStyledItemDelegate
{
public:
FilterDelegate(QObject *filter, QObject *parent = 0) :
QStyledItemDelegate(parent), filter(filter)
{ }
virtual QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QWidget *editor = QStyledItemDelegate::createEditor(parent, option, index);
editor->installEventFilter(filter);
return editor;
}
private:
QObject *filter;
};
Then your MainWindow constructor would look something like this:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
setupUi(this);
tableWidget->setItemDelegate(new FilterDelegate(this));
tableWidget->installEventFilter(this);
}
And your event filter:
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if(event->type() == QEvent::KeyPress)
{
// do something
}
return QMainWindow::eventFilter(obj, event);
}
ANOTHER ALTERNATIVE:
You can install event filter on the QApplication object and capture all events. This is a bit of an overkill if you ask me, but it would work for a small application and requires minimal code.
All you have to do is:
qApp->installEventFilter(this);
And:
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if(event->type() == QEvent::KeyPress)
{
// do something
}
return QMainWindow::eventFilter(obj, event);
}

QTreeView custom column

I need show files from QFileSystemModel in QTreeView and customize that tree to show one more column with QCheckBox, so user can pick 0..N files from that QTreeView.
I read doc from Qt to understand model/view architecture and i am now in my code at point, where i have custom delegate CustomItemDelegatefor specific column, but actually i don't know how to create QCheckBox in paint method of my custom delegate (to be more specific i know how, but this is 99% bad way).
customitemdelegate.h
#ifndef CUSTOMITEMDELEGATE_H
#define CUSTOMITEMDELEGATE_H
#include <QStyledItemDelegate>
class CustomItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit CustomItemDelegate(QObject *parent = 0);
void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const;
signals:
public slots:
};
#endif // CUSTOMITEMDELEGATE_H
customitemdelegate.cpp
#include "customitemdelegate.h"
#include <QCheckBox>
#include <iostream>
#include <QTreeView>
using namespace std;
CustomItemDelegate::CustomItemDelegate(QObject *parent) :
QStyledItemDelegate(parent)
{
}
void CustomItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const {
((QTreeView *)parent())->setIndexWidget(index, new QCheckBox());
}
You don't create a QCheckbox, you paint one using the current style. Look at the docs regarding QStyle, specifically for drawControl(..). There's also a customised example I wrote for a question on SO which you can get an idea from.
Mouse handling has to be handled by the view (because the control doesn't actually exist), and for most styles that will include mouse-over updating.
It is a bit of a pain (things may have gotten easier in v5.0+, I last did this in v4.8), but it's well worth it. Creating 'real' QCheckBoxs is inefficient (in your example it will cause a massive memory leak), and becomes noticeably slow for large datasets. Whereas painting a 'fake' one only when required (i.e. visible) is very fast.

How set display format and similar properties of QDateTimeEdit in a QTreeView?

How can I set the - say - displayFormat and the calendarPopup property of QDateTimeEdit objects that are used in a QTreeView?
(They are used when editing a QVariant(QDateTime) value there.)
Is it possible to use Qt's property system for that purpose?
Unfortunately, the Style Sheets Reference does not list those properties for QDateTimeEdit. On the other hand, the documentation mentions that:
From 4.3 and above, any designable Q_PROPERTY can be set using the qproperty- syntax.
Thus, I've tried something like this:
QApplication app(argc, argv);
// first try
// app.setStyleSheet(
// " QDateTimeEdit { displayFormat: \"yyyy-MM-dd hh:mm:ss\" ; }");
app.setStyleSheet(
" QDateTimeEdit { qproperty-displayFormat: \"yyyy-MM-dd hh:mm:ss\" ; }");
Both style-sheets are not picked up, though.
What is the correct stylesheet syntax for setting those properties?
Or is there another way to set default values for those properties in an application?
The QDateTime display format used in a TableView, TreeView or ListView can be modified by modifying the ItemDelegate.
In this method, we derive StyledItemDelegate to override createEditor and displayText methods and then apply the new delegate to the desired view.
DateFormatDelegate.hpp :
#include <qstyleditemdelegate.h>
class DateFormatDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit DateFormatDelegate(QObject* parent = Q_NULLPTR)
:QStyledItemDelegate(parent) {}
QString displayText(const QVariant& value, const QLocale& locale) const Q_DECL_OVERRIDE;
QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
DateFormatDelegate.cpp :
QString DateFormatDelegate::displayText(const QVariant& value, const QLocale& locale) const
{
switch (value.type()) {
case QVariant::DateTime:
return locale.toString(value.toDateTime(), "yyyy/MM/dd hh:mm:ss");
default:
return QStyledItemDelegate::displayText(value, locale);
}
}
QWidget *DateFormatDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QWidget* widget = QStyledItemDelegate::createEditor(parent, option,index);
if( strcmp(widget->metaObject()->className(),"QDateTimeEdit") == 0)
dynamic_cast<QDateTimeEdit*>(widget)->setDisplayFormat("yyyy/MM/dd hh:mm:ss");
return widget;
}
You can then set the new delegate to your view :
ui->TableView_MyTable->setIteemDelegate(
new DateFormatDelegate(ui->TableView_MyTable)));
it can also be applied directly in the constructor if you have derived the view.
This method allows to modify any editor widget's style without worrying of later modification or your view organization.
There might be more elegant solutions though.

How to set a delegate for a single cell in Qt item view?

Rather perplexed by this omission--but in Qt's QAbstractItemView class, it's possible to set a QAbstractItemDelegate (i.e., QItemDelegate or QStyledItemDelegate) to the entire view, a single row, or a single column, using the setItemDelegate* methods. In addition the item delegate for an individual cell can be queried, with QAbstractItemView::itemDelegate(const QModelIndex&), along with the delegate for rows, columns. and the entire view. But there appears to be no way to set an item delegate to an individual cell. Am I missing something? Any reason this should be?
No you can't set item delegate only for one cell or one column but you can easly set item delegate for whole widget and choose in which cell, column or row you want to use your custom painting or something.
For e.g.
void WidgetDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == 1)
{
// ohh it's my column
// better do something creative
}
else // it's just a common column. Live it in default way
QItemDelegate::paint(painter, option, index);
}
You can find some more information here
I'd recommend reimplementing createEditor function instead:
QWidget * WidgetDelegate::createEditor(
QWidget *parent,
const QStyleOptionViewItem &,
const QModelIndex &index) const
{
QWidget *widget = 0;
if (index.isValid() && index.column() < factories.size())
{
widget = factories[index.column()]->createEditor(index.data(Qt::EditRole).userType(), parent);
if (widget)
widget->setFocusPolicy(Qt::WheelFocus);
}
return widget;
}
Sinc Qt 4.2, QAbstractItemView provides setItemDelegateForColumn() and setItemDelegateForRow().